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::de::read::Reader;
58use bincode::de::{Decoder, DecoderImpl};
59use bincode::enc::EncoderImpl;
60use bincode::enc::write::{SizeWriter, Writer};
61use bincode::error::{DecodeError, EncodeError};
62use bincode::{Decode, Encode};
63#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
64use core::alloc::Layout;
65use core::fmt::Result as FmtResult;
66use core::fmt::{Debug, Formatter};
67use core::marker::PhantomData;
68
69#[cfg(all(feature = "std", feature = "async-cl-io"))]
70use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, sync_channel};
71#[cfg(all(feature = "std", feature = "async-cl-io"))]
72use std::thread::JoinHandle;
73
74#[cfg(feature = "std")]
75#[doc(hidden)]
76pub type TasksInstantiator<CT> = for<'c> fn(
77 Vec<Option<&'c ComponentConfig>>,
78 &mut ResourceManager,
79 &[Option<Arc<ThreadPool>>],
80) -> CuResult<CT>;
81#[cfg(not(feature = "std"))]
82#[doc(hidden)]
83pub type TasksInstantiator<CT> =
84 for<'c> fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>;
85#[doc(hidden)]
86pub type BridgesInstantiator<CB> = fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>;
87#[cfg(feature = "std")]
94#[doc(hidden)]
95pub type ThreadPoolsInstantiator = fn(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>;
96#[doc(hidden)]
97pub type MonitorInstantiator<M> = fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M;
98
99#[doc(hidden)]
100pub struct CuRuntimeParts<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI> {
101 pub tasks_instanciator: TI,
102 pub monitored_components: &'static [MonitorComponentMetadata],
103 pub culist_component_mapping: &'static [ComponentId],
104 #[cfg(all(feature = "std", feature = "parallel-rt"))]
105 pub parallel_rt_metadata: &'static ParallelRtMetadata,
106 pub monitor_instanciator: MI,
107 pub bridges_instanciator: BI,
108 _payload: PhantomData<(CT, CB, P, M, [(); NBCL])>,
109}
110
111impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI>
112 CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>
113{
114 pub const fn new(
115 tasks_instanciator: TI,
116 monitored_components: &'static [MonitorComponentMetadata],
117 culist_component_mapping: &'static [ComponentId],
118 #[cfg(all(feature = "std", feature = "parallel-rt"))]
119 parallel_rt_metadata: &'static ParallelRtMetadata,
120 monitor_instanciator: MI,
121 bridges_instanciator: BI,
122 ) -> Self {
123 Self {
124 tasks_instanciator,
125 monitored_components,
126 culist_component_mapping,
127 #[cfg(all(feature = "std", feature = "parallel-rt"))]
128 parallel_rt_metadata,
129 monitor_instanciator,
130 bridges_instanciator,
131 _payload: PhantomData,
132 }
133 }
134}
135
136#[doc(hidden)]
137pub struct CuRuntimeBuilder<
138 'cfg,
139 CT,
140 CB,
141 P: CopperListTuple,
142 M: CuMonitor,
143 const NBCL: usize,
144 TI,
145 BI,
146 MI,
147 CLW,
148 KFW,
149> {
150 clock: RobotClock,
151 config: &'cfg CuConfig,
152 mission: &'cfg str,
153 subsystem: Subsystem,
154 instance_id: u32,
155 resources: Option<ResourceManager>,
156 #[cfg(feature = "std")]
157 thread_pools: Option<Vec<Option<Arc<ThreadPool>>>>,
158 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
159 copperlists_logger: CLW,
160 keyframes_logger: KFW,
161}
162
163impl<'cfg, CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI, CLW, KFW>
164 CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLW, KFW>
165{
166 pub fn new(
167 clock: RobotClock,
168 config: &'cfg CuConfig,
169 mission: &'cfg str,
170 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
171 copperlists_logger: CLW,
172 keyframes_logger: KFW,
173 ) -> Self {
174 Self {
175 clock,
176 config,
177 mission,
178 subsystem: Subsystem::new(None, 0),
179 instance_id: 0,
180 resources: None,
181 #[cfg(feature = "std")]
182 thread_pools: None,
183 parts,
184 copperlists_logger,
185 keyframes_logger,
186 }
187 }
188
189 pub fn with_subsystem(mut self, subsystem: Subsystem) -> Self {
190 self.subsystem = subsystem;
191 self
192 }
193
194 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
195 self.instance_id = instance_id;
196 self
197 }
198
199 pub fn with_resources(mut self, resources: ResourceManager) -> Self {
200 self.resources = Some(resources);
201 self
202 }
203
204 pub fn try_with_resources_instantiator(
205 mut self,
206 resources_instantiator: impl FnOnce(&CuConfig) -> CuResult<ResourceManager>,
207 ) -> CuResult<Self> {
208 self.resources = Some(resources_instantiator(self.config)?);
209 Ok(self)
210 }
211
212 #[cfg(feature = "std")]
216 pub fn with_thread_pools(mut self, pools: Vec<Option<Arc<ThreadPool>>>) -> Self {
217 self.thread_pools = Some(pools);
218 self
219 }
220
221 #[cfg(feature = "std")]
222 pub fn try_with_thread_pools_instantiator(
223 mut self,
224 thread_pools_instantiator: impl FnOnce(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>,
225 ) -> CuResult<Self> {
226 self.thread_pools = Some(thread_pools_instantiator(self.config)?);
227 Ok(self)
228 }
229}
230
231#[inline]
242pub fn perf_now(_clock: &RobotClock) -> CuTime {
243 #[cfg(all(feature = "std", feature = "sysclock-perf"))]
244 {
245 static PERF_CLOCK: std::sync::OnceLock<RobotClock> = std::sync::OnceLock::new();
246 return PERF_CLOCK.get_or_init(RobotClock::new).now();
247 }
248
249 #[allow(unreachable_code)]
250 _clock.now()
251}
252
253#[cfg(all(feature = "std", feature = "high-precision-limiter"))]
254const HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS: u64 = 200_000;
255
256#[inline]
258pub fn rate_target_period(rate_target_hz: u64) -> CuResult<CuDuration> {
259 if rate_target_hz == 0 {
260 return Err(CuError::from(
261 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
262 ));
263 }
264
265 if rate_target_hz > MAX_RATE_TARGET_HZ {
266 return Err(CuError::from(format!(
267 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
268 )));
269 }
270
271 Ok(CuDuration::from(MAX_RATE_TARGET_HZ / rate_target_hz))
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub struct LoopRateLimiter {
282 period: CuDuration,
283 next_deadline: CuTime,
284}
285
286impl LoopRateLimiter {
287 #[inline]
288 pub fn from_rate_target_hz(rate_target_hz: u64, clock: &RobotClock) -> CuResult<Self> {
289 let period = rate_target_period(rate_target_hz)?;
290 Ok(Self {
291 period,
292 next_deadline: clock.now() + period,
293 })
294 }
295
296 #[inline]
297 pub fn is_ready(&self, clock: &RobotClock) -> bool {
298 self.remaining(clock).is_none()
299 }
300
301 #[inline]
302 pub fn remaining(&self, clock: &RobotClock) -> Option<CuDuration> {
303 let now = clock.now();
304 if now < self.next_deadline {
305 Some(self.next_deadline - now)
306 } else {
307 None
308 }
309 }
310
311 #[inline]
312 pub fn wait_until_ready(&self, clock: &RobotClock) {
313 let deadline = self.next_deadline;
314 let Some(remaining) = self.remaining(clock) else {
315 return;
316 };
317
318 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
319 {
320 let spin_window = self.spin_window();
321 if remaining > spin_window {
322 std::thread::sleep(std::time::Duration::from(remaining - spin_window));
323 }
324 while clock.now() < deadline {
325 core::hint::spin_loop();
326 }
327 }
328
329 #[cfg(all(feature = "std", not(feature = "high-precision-limiter")))]
330 {
331 let _ = deadline;
332 std::thread::sleep(std::time::Duration::from(remaining));
333 }
334
335 #[cfg(not(feature = "std"))]
336 {
337 let _ = remaining;
338 while clock.now() < deadline {
339 core::hint::spin_loop();
340 }
341 }
342 }
343
344 #[inline]
345 pub fn mark_tick(&mut self, clock: &RobotClock) {
346 self.advance_from(clock.now());
347 }
348
349 #[inline]
350 pub fn limit(&mut self, clock: &RobotClock) {
351 self.wait_until_ready(clock);
352 self.mark_tick(clock);
353 }
354
355 #[inline]
356 fn advance_from(&mut self, now: CuTime) {
357 let steps = if now < self.next_deadline {
358 1
359 } else {
360 (now - self.next_deadline).as_nanos() / self.period.as_nanos() + 1
361 };
362 self.next_deadline += steps * self.period;
363 }
364
365 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
366 #[inline]
367 fn spin_window(&self) -> CuDuration {
368 let _ = self.period;
369 CuDuration::from(HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS)
370 }
371
372 #[cfg(test)]
373 #[inline]
374 fn next_deadline(&self) -> CuTime {
375 self.next_deadline
376 }
377}
378
379#[cfg(all(feature = "std", feature = "async-cl-io"))]
380#[doc(hidden)]
381pub trait AsyncCopperListPayload: Send {}
382
383#[cfg(all(feature = "std", feature = "async-cl-io"))]
384impl<T: Send> AsyncCopperListPayload for T {}
385
386#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
387#[doc(hidden)]
388pub trait AsyncCopperListPayload {}
389
390#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
391impl<T> AsyncCopperListPayload for T {}
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq)]
400#[doc(hidden)]
401pub enum ProcessStepOutcome {
402 Continue,
403 AbortCopperList,
404}
405
406#[doc(hidden)]
408pub type ProcessStepResult = CuResult<ProcessStepOutcome>;
409
410#[cfg(feature = "remote-debug")]
411fn encode_completed_copperlist_snapshot<P: CopperListTuple>(
412 cl: &CopperList<P>,
413) -> CuResult<Vec<u8>> {
414 bincode::encode_to_vec(cl, bincode::config::standard())
415 .map_err(|e| CuError::new_with_cause("Failed to encode completed CopperList snapshot", e))
416}
417
418#[doc(hidden)]
420pub struct SyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
421 inner: CuListsManager<P, NBCL>,
422 logger: Option<Box<dyn WriteStream<CopperList<P>>>>,
424 #[cfg(feature = "remote-debug")]
426 last_completed_encoded: Option<Vec<u8>>,
427 pub last_encoded_bytes: u64,
429 pub last_handle_bytes: u64,
431}
432
433impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
434 pub fn new(logger: Option<Box<dyn WriteStream<CopperList<P>>>>) -> CuResult<Self>
435 where
436 P: CuListZeroedInit,
437 {
438 Ok(Self {
439 inner: CuListsManager::new(),
440 logger,
441 #[cfg(feature = "remote-debug")]
442 last_completed_encoded: None,
443 last_encoded_bytes: 0,
444 last_handle_bytes: 0,
445 })
446 }
447
448 pub fn next_cl_id(&self) -> u64 {
449 self.inner.next_cl_id()
450 }
451
452 pub fn last_cl_id(&self) -> u64 {
453 self.inner.last_cl_id()
454 }
455
456 pub fn peek(&self) -> Option<&CopperList<P>> {
457 self.inner.peek()
458 }
459
460 #[cfg(feature = "remote-debug")]
461 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
462 self.last_completed_encoded.as_deref()
463 }
464
465 #[cfg(not(feature = "remote-debug"))]
466 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
467 None
468 }
469
470 #[cfg(feature = "remote-debug")]
471 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
472 self.last_completed_encoded = snapshot;
473 }
474
475 #[cfg(not(feature = "remote-debug"))]
476 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
477
478 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
479 where
480 P: CuListZeroedInit,
481 {
482 self.inner
483 .create()
484 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))
485 }
486
487 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
488 #[cfg(debug_assertions)]
489 self.debug_assert_end_of_processing_target(culistid);
490
491 let mut is_top = true;
492 let mut nb_done = 0;
493 self.last_encoded_bytes = 0;
494 self.last_handle_bytes = 0;
495 #[cfg(feature = "remote-debug")]
496 let last_completed_encoded = &mut self.last_completed_encoded;
497 for cl in self.inner.iter_mut() {
498 if cl.id == culistid && cl.get_state() == CopperListState::Processing {
499 cl.change_state(CopperListState::DoneProcessing);
500 #[cfg(feature = "remote-debug")]
501 {
502 *last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
503 }
504 }
505 if is_top && cl.get_state() == CopperListState::DoneProcessing {
506 if let Some(logger) = &mut self.logger {
507 cl.change_state(CopperListState::BeingSerialized);
508 logger.log(cl)?;
509 self.last_encoded_bytes = logger.last_log_bytes().unwrap_or(0) as u64;
510 self.last_handle_bytes = take_last_completed_handle_bytes();
511 }
512 cl.change_state(CopperListState::Free);
513 nb_done += 1;
514 } else {
515 is_top = false;
516 }
517 }
518 for _ in 0..nb_done {
519 let _ = self.inner.pop();
520 }
521 Ok(())
522 }
523
524 pub fn finish_pending(&mut self) -> CuResult<()> {
525 Ok(())
526 }
527
528 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
529 Ok(NBCL - self.inner.len())
530 }
531
532 #[cfg(feature = "std")]
533 pub fn end_of_processing_boxed(
534 &mut self,
535 mut culist: Box<CopperList<P>>,
536 ) -> CuResult<OwnedCopperListSubmission<P>> {
537 #[cfg(debug_assertions)]
538 debug_assert_processing_completion_state(culist.as_ref(), "sync boxed end_of_processing");
539
540 culist.change_state(CopperListState::DoneProcessing);
541 self.last_encoded_bytes = 0;
542 self.last_handle_bytes = 0;
543 if let Some(logger) = &mut self.logger {
544 culist.change_state(CopperListState::BeingSerialized);
545 logger.log(&culist)?;
546 self.last_encoded_bytes = logger.last_log_bytes().unwrap_or(0) as u64;
547 self.last_handle_bytes = take_last_completed_handle_bytes();
548 }
549 culist.change_state(CopperListState::Free);
550 Ok(OwnedCopperListSubmission::Recycled(culist))
551 }
552
553 #[cfg(feature = "std")]
554 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
555 Ok(None)
556 }
557
558 #[cfg(feature = "std")]
559 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
560 Err(CuError::from(
561 "Synchronous CopperList I/O cannot block waiting for boxed completions",
562 ))
563 }
564
565 #[cfg(feature = "std")]
566 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
567 Ok(Vec::new())
568 }
569
570 #[cfg(debug_assertions)]
571 fn debug_assert_end_of_processing_target(&self, culistid: u64) {
572 let mut matches = 0usize;
573 let mut state = None;
574 for cl in self.inner.iter() {
575 if cl.id == culistid {
576 matches += 1;
577 state = Some(cl.get_state());
578 }
579 }
580
581 assert_eq!(
582 matches, 1,
583 "sync end_of_processing expected exactly one active CopperList #{culistid}, found {matches}"
584 );
585 assert_eq!(
586 state,
587 Some(CopperListState::Processing),
588 "sync end_of_processing expected CopperList #{culistid} to be Processing, found {:?}",
589 state
590 );
591 }
592}
593
594#[cfg(feature = "std")]
596#[doc(hidden)]
597pub enum OwnedCopperListSubmission<P: CopperListTuple> {
598 Recycled(Box<CopperList<P>>),
600 Pending,
602}
603
604#[cfg(all(feature = "std", feature = "async-cl-io"))]
605struct AsyncCopperListCompletion<P: CopperListTuple> {
606 culist: Box<CopperList<P>>,
607 log_result: CuResult<(u64, u64)>,
608}
609
610#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
611fn allocate_zeroed_copperlist<P>() -> Box<CopperList<P>>
612where
613 P: CopperListTuple + CuListZeroedInit,
614{
615 let mut culist = unsafe {
617 let layout = Layout::new::<CopperList<P>>();
618 let ptr = alloc_zeroed(layout) as *mut CopperList<P>;
619 if ptr.is_null() {
620 handle_alloc_error(layout);
621 }
622 Box::from_raw(ptr)
623 };
624 culist.msgs.init_zeroed();
625 culist
626}
627
628#[cfg(all(feature = "std", feature = "parallel-rt"))]
629pub fn allocate_boxed_copperlists<P, const NBCL: usize>() -> Vec<Box<CopperList<P>>>
630where
631 P: CopperListTuple + CuListZeroedInit,
632{
633 let mut free_pool = Vec::with_capacity(NBCL);
634 for _ in 0..NBCL {
635 free_pool.push(allocate_zeroed_copperlist::<P>());
636 }
637 free_pool
638}
639
640#[cfg(all(feature = "std", feature = "async-cl-io"))]
642#[doc(hidden)]
643pub struct AsyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
644 free_pool: Vec<Box<CopperList<P>>>,
645 current: Option<Box<CopperList<P>>>,
646 #[cfg(feature = "remote-debug")]
647 last_completed_encoded: Option<Vec<u8>>,
648 pending_count: usize,
649 next_cl_id: u64,
650 pending_sender: Option<SyncSender<Box<CopperList<P>>>>,
651 completion_receiver: Option<Receiver<AsyncCopperListCompletion<P>>>,
652 worker_handle: Option<JoinHandle<()>>,
653 pub last_encoded_bytes: u64,
655 pub last_handle_bytes: u64,
657}
658
659#[cfg(all(feature = "std", feature = "async-cl-io"))]
660impl<P: CopperListTuple + Default, const NBCL: usize> AsyncCopperListsManager<P, NBCL> {
661 pub fn new(logger: Option<Box<dyn WriteStream<CopperList<P>>>>) -> CuResult<Self>
662 where
663 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
664 {
665 let mut free_pool = Vec::with_capacity(NBCL);
666 for _ in 0..NBCL {
667 free_pool.push(allocate_zeroed_copperlist::<P>());
668 }
669
670 let (pending_sender, completion_receiver, worker_handle) = if let Some(mut logger) = logger
671 {
672 let (pending_sender, pending_receiver) = sync_channel::<Box<CopperList<P>>>(NBCL);
673 let (completion_sender, completion_receiver) =
674 sync_channel::<AsyncCopperListCompletion<P>>(NBCL);
675 let worker_handle = std::thread::Builder::new()
676 .name("cu-async-cl-io".to_string())
677 .spawn(move || {
678 while let Ok(mut culist) = pending_receiver.recv() {
679 culist.change_state(CopperListState::BeingSerialized);
680 let log_result = logger.log(&culist).map(|_| {
681 (
682 logger.last_log_bytes().unwrap_or(0) as u64,
683 take_last_completed_handle_bytes(),
684 )
685 });
686 let should_stop = log_result.is_err();
687 if completion_sender
688 .send(AsyncCopperListCompletion { culist, log_result })
689 .is_err()
690 {
691 break;
692 }
693 if should_stop {
694 break;
695 }
696 }
697 })
698 .map_err(|e| {
699 CuError::from("Failed to spawn async CopperList serializer thread")
700 .add_cause(e.to_string().as_str())
701 })?;
702 (
703 Some(pending_sender),
704 Some(completion_receiver),
705 Some(worker_handle),
706 )
707 } else {
708 (None, None, None)
709 };
710
711 Ok(Self {
712 free_pool,
713 current: None,
714 #[cfg(feature = "remote-debug")]
715 last_completed_encoded: None,
716 pending_count: 0,
717 next_cl_id: 0,
718 pending_sender,
719 completion_receiver,
720 worker_handle,
721 last_encoded_bytes: 0,
722 last_handle_bytes: 0,
723 })
724 }
725
726 pub fn next_cl_id(&self) -> u64 {
727 self.next_cl_id
728 }
729
730 pub fn last_cl_id(&self) -> u64 {
731 self.next_cl_id.saturating_sub(1)
732 }
733
734 pub fn peek(&self) -> Option<&CopperList<P>> {
735 self.current.as_deref()
736 }
737
738 #[cfg(feature = "remote-debug")]
739 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
740 self.last_completed_encoded.as_deref()
741 }
742
743 #[cfg(not(feature = "remote-debug"))]
744 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
745 None
746 }
747
748 #[cfg(feature = "remote-debug")]
749 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
750 self.last_completed_encoded = snapshot;
751 }
752
753 #[cfg(not(feature = "remote-debug"))]
754 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
755
756 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
757 where
758 P: CuListZeroedInit,
759 {
760 if self.current.is_some() {
761 return Err(CuError::from(
762 "Attempted to create a CopperList while another one is still active",
763 ));
764 }
765
766 self.reclaim_completed()?;
767 while self.free_pool.is_empty() {
768 self.wait_for_completion()?;
769 }
770
771 let culist = self
772 .free_pool
773 .pop()
774 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))?;
775 self.current = Some(culist);
776
777 let current = self
778 .current
779 .as_mut()
780 .expect("current CopperList is missing");
781 current.reset_for_runtime_use(self.next_cl_id);
782 self.next_cl_id += 1;
783 Ok(current.as_mut())
784 }
785
786 #[cfg(feature = "remote-debug")]
787 fn capture_completed_snapshot(&mut self, cl: &CopperList<P>) -> CuResult<()> {
788 self.last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
789 Ok(())
790 }
791
792 #[cfg(not(feature = "remote-debug"))]
793 fn capture_completed_snapshot(&mut self, _cl: &CopperList<P>) -> CuResult<()> {
794 Ok(())
795 }
796
797 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
798 self.reclaim_completed()?;
799
800 let mut culist = self.current.take().ok_or_else(|| {
801 CuError::from("Attempted to finish processing without an active CopperList")
802 })?;
803
804 if culist.id != culistid {
805 return Err(CuError::from(format!(
806 "Attempted to finish CopperList #{culistid} while CopperList #{} is active",
807 culist.id
808 )));
809 }
810 #[cfg(debug_assertions)]
811 debug_assert_processing_completion_state(culist.as_ref(), "async end_of_processing");
812
813 culist.change_state(CopperListState::DoneProcessing);
814 self.capture_completed_snapshot(&culist)?;
815 self.last_encoded_bytes = 0;
816 self.last_handle_bytes = 0;
817
818 if let Some(pending_sender) = &self.pending_sender {
819 culist.change_state(CopperListState::QueuedForSerialization);
820 pending_sender.send(culist).map_err(|e| {
821 CuError::from("Failed to enqueue CopperList for async serialization")
822 .add_cause(e.to_string().as_str())
823 })?;
824 self.pending_count += 1;
825 self.reclaim_completed()?;
826 } else {
827 culist.change_state(CopperListState::Free);
828 self.free_pool.push(culist);
829 }
830
831 Ok(())
832 }
833
834 pub fn finish_pending(&mut self) -> CuResult<()> {
835 if self.current.is_some() {
836 return Err(CuError::from(
837 "Cannot flush CopperList I/O while a CopperList is still active",
838 ));
839 }
840
841 while self.pending_count > 0 {
842 self.wait_for_completion()?;
843 }
844 Ok(())
845 }
846
847 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
848 self.reclaim_completed()?;
849 Ok(self.free_pool.len())
850 }
851
852 pub fn end_of_processing_boxed(
853 &mut self,
854 mut culist: Box<CopperList<P>>,
855 ) -> CuResult<OwnedCopperListSubmission<P>> {
856 self.reclaim_completed()?;
857 #[cfg(debug_assertions)]
858 debug_assert_processing_completion_state(culist.as_ref(), "async boxed end_of_processing");
859 culist.change_state(CopperListState::DoneProcessing);
860 self.capture_completed_snapshot(&culist)?;
861 self.last_encoded_bytes = 0;
862 self.last_handle_bytes = 0;
863
864 if let Some(pending_sender) = &self.pending_sender {
865 culist.change_state(CopperListState::QueuedForSerialization);
866 pending_sender.send(culist).map_err(|e| {
867 CuError::from("Failed to enqueue CopperList for async serialization")
868 .add_cause(e.to_string().as_str())
869 })?;
870 self.pending_count += 1;
871 self.reclaim_completed()?;
872 Ok(OwnedCopperListSubmission::Pending)
873 } else {
874 culist.change_state(CopperListState::Free);
875 Ok(OwnedCopperListSubmission::Recycled(culist))
876 }
877 }
878
879 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
880 let recv_result = {
881 let Some(completion_receiver) = self.completion_receiver.as_ref() else {
882 return Ok(None);
883 };
884 completion_receiver.try_recv()
885 };
886 match recv_result {
887 Ok(completion) => self.handle_completion(completion).map(Some),
888 Err(TryRecvError::Empty) => Ok(None),
889 Err(TryRecvError::Disconnected) => Err(CuError::from(
890 "Async CopperList serializer thread disconnected unexpectedly",
891 )),
892 }
893 }
894
895 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
896 let completion = self
897 .completion_receiver
898 .as_ref()
899 .ok_or_else(|| {
900 CuError::from("No async CopperList serializer is active to return a free slot")
901 })?
902 .recv()
903 .map_err(|e| {
904 CuError::from("Failed to receive completion from async CopperList serializer")
905 .add_cause(e.to_string().as_str())
906 })?;
907 self.handle_completion(completion)
908 }
909
910 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
911 let mut reclaimed = Vec::with_capacity(self.pending_count);
912 if self.current.is_some() {
913 return Err(CuError::from(
914 "Cannot flush CopperList I/O while a CopperList is still active",
915 ));
916 }
917 while self.pending_count > 0 {
918 reclaimed.push(self.wait_reclaim_boxed()?);
919 }
920 Ok(reclaimed)
921 }
922
923 fn reclaim_completed(&mut self) -> CuResult<()> {
924 loop {
925 let Some(culist) = self.try_reclaim_boxed()? else {
926 break;
927 };
928 self.free_pool.push(culist);
929 }
930 Ok(())
931 }
932
933 fn wait_for_completion(&mut self) -> CuResult<()> {
934 let culist = self.wait_reclaim_boxed()?;
935 self.free_pool.push(culist);
936 Ok(())
937 }
938
939 fn handle_completion(
940 &mut self,
941 mut completion: AsyncCopperListCompletion<P>,
942 ) -> CuResult<Box<CopperList<P>>> {
943 self.pending_count = self.pending_count.saturating_sub(1);
944 if let Ok((encoded_bytes, handle_bytes)) = completion.log_result.as_ref() {
945 self.last_encoded_bytes = *encoded_bytes;
946 self.last_handle_bytes = *handle_bytes;
947 }
948 completion.culist.change_state(CopperListState::Free);
949 completion.log_result?;
950 Ok(completion.culist)
951 }
952
953 fn shutdown_worker(&mut self) -> CuResult<()> {
954 self.finish_pending()?;
955 self.pending_sender.take();
956 if let Some(worker_handle) = self.worker_handle.take() {
957 worker_handle.join().map_err(|_| {
958 CuError::from("Async CopperList serializer thread panicked while joining")
959 })?;
960 }
961 Ok(())
962 }
963}
964
965#[cfg(all(feature = "std", feature = "async-cl-io"))]
966impl<P: CopperListTuple + Default, const NBCL: usize> Drop for AsyncCopperListsManager<P, NBCL> {
967 fn drop(&mut self) {
968 let _ = self.shutdown_worker();
969 }
970}
971
972#[cfg(all(feature = "std", debug_assertions))]
973fn debug_assert_processing_completion_state<P: CopperListTuple>(
974 culist: &CopperList<P>,
975 context: &str,
976) {
977 assert_eq!(
978 culist.get_state(),
979 CopperListState::Processing,
980 "{context} expected CopperList #{} to be Processing, found {}",
981 culist.id,
982 culist.get_state()
983 );
984}
985
986#[cfg(all(feature = "std", feature = "async-cl-io"))]
987#[doc(hidden)]
988pub type CopperListsManager<P, const NBCL: usize> = AsyncCopperListsManager<P, NBCL>;
989
990#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
991#[doc(hidden)]
992pub type CopperListsManager<P, const NBCL: usize> = SyncCopperListsManager<P, NBCL>;
993
994pub struct KeyFramesManager {
996 inner: KeyFrame,
998
999 forced_timestamp: Option<CuTime>,
1001
1002 locked: bool,
1004
1005 logger: Option<Box<dyn WriteStream<KeyFrame>>>,
1007
1008 keyframe_interval: u32,
1010
1011 pub last_encoded_bytes: u64,
1013
1014 capture_size_hint: usize,
1016}
1017
1018const MIN_KEYFRAME_CAPTURE_CAPACITY: usize = 4 * 1024;
1019
1020struct PreallocatedVecWriter<'a>(&'a mut Vec<u8>);
1022
1023impl Writer for PreallocatedVecWriter<'_> {
1024 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
1025 if bytes.len() > self.0.capacity().saturating_sub(self.0.len()) {
1026 return Err(EncodeError::UnexpectedEnd);
1027 }
1028 self.0.extend_from_slice(bytes);
1030 Ok(())
1031 }
1032}
1033
1034impl KeyFramesManager {
1035 fn is_keyframe(&self, culistid: u64) -> bool {
1036 self.logger.is_some() && culistid.is_multiple_of(self.keyframe_interval as u64)
1037 }
1038
1039 #[inline]
1040 pub fn captures_keyframe(&self, culistid: u64) -> bool {
1041 self.is_keyframe(culistid)
1042 }
1043
1044 #[doc(hidden)]
1046 pub fn begin_capture_preallocation(&mut self) {
1047 self.capture_size_hint = KEYFRAME_PAYLOAD_HEADER.len();
1048 }
1049
1050 #[doc(hidden)]
1052 pub fn include_capture_capacity(&mut self, item: &impl Freezable) -> CuResult<()> {
1053 if self.logger.is_none() {
1054 return Ok(());
1055 }
1056 let mut sizer = EncoderImpl::new(SizeWriter::default(), bincode::config::standard());
1057 BincodeAdapter(item)
1058 .encode(&mut sizer)
1059 .map_err(|_| CuError::from("Failed to size component keyframe state"))?;
1060 let payload_bytes = sizer.into_writer().bytes_written as usize;
1061 self.capture_size_hint = self
1062 .capture_size_hint
1063 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
1064 .and_then(|size| size.checked_add(payload_bytes))
1065 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1066 Ok(())
1067 }
1068
1069 #[doc(hidden)]
1071 pub fn finish_capture_preallocation(&mut self) -> CuResult<()> {
1072 if self.logger.is_none() {
1073 return Ok(());
1074 }
1075 let requested = self
1076 .capture_size_hint
1077 .max(MIN_KEYFRAME_CAPTURE_CAPACITY)
1078 .checked_next_power_of_two()
1079 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1080 if self.inner.serialized_tasks.capacity() < requested {
1081 let additional = requested.saturating_sub(self.inner.serialized_tasks.len());
1082 self.inner
1083 .serialized_tasks
1084 .try_reserve_exact(additional)
1085 .map_err(|error| {
1086 CuError::from("Failed to preallocate keyframe capture buffer")
1087 .add_cause(&error.to_string())
1088 })?;
1089 }
1090 Ok(())
1091 }
1092
1093 pub fn reset(&mut self, culistid: u64, clock: &RobotClock) {
1094 if self.is_keyframe(culistid) {
1095 if self.locked && self.inner.culistid == culistid {
1097 return;
1098 }
1099 let ts = self.forced_timestamp.take().unwrap_or_else(|| clock.now());
1100 self.inner.reset(culistid, ts);
1101 self.locked = false;
1102 }
1103 }
1104
1105 #[cfg(feature = "std")]
1107 pub fn set_forced_timestamp(&mut self, ts: CuTime) {
1108 self.forced_timestamp = Some(ts);
1109 }
1110
1111 pub fn freeze_task(&mut self, culistid: u64, task: &impl Freezable) -> CuResult<usize> {
1112 if self.is_keyframe(culistid) {
1113 if self.locked {
1114 return Ok(0);
1116 }
1117 if self.inner.culistid != culistid {
1118 return Err(CuError::from(format!(
1119 "Freezing task for culistid {} but current keyframe is {}",
1120 culistid, self.inner.culistid
1121 )));
1122 }
1123 let encoded = self
1124 .inner
1125 .add_frozen_task(task)
1126 .map_err(|e| CuError::from(format!("Failed to serialize task: {e}")))?;
1127 Ok(encoded)
1128 } else {
1129 Ok(0)
1130 }
1131 }
1132
1133 pub fn freeze_any(&mut self, culistid: u64, item: &impl Freezable) -> CuResult<usize> {
1135 self.freeze_task(culistid, item)
1136 }
1137
1138 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1139 if self.is_keyframe(culistid) {
1140 let logger = self.logger.as_mut().unwrap();
1141 logger.log(&self.inner)?;
1142 self.last_encoded_bytes = logger.last_log_bytes().unwrap_or(0) as u64;
1143 self.locked = false;
1145 Ok(())
1146 } else {
1147 self.last_encoded_bytes = 0;
1149 Ok(())
1150 }
1151 }
1152
1153 #[cfg(feature = "std")]
1155 pub fn lock_keyframe(&mut self, keyframe: &KeyFrame) {
1156 self.inner = keyframe.clone();
1157 self.forced_timestamp = Some(keyframe.timestamp);
1158 self.locked = true;
1159 }
1160}
1161
1162pub struct CuRuntime<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> {
1166 clock: RobotClock,
1168
1169 subsystem_code: u16,
1171
1172 #[doc(hidden)]
1174 pub instance_id: u32,
1175
1176 #[doc(hidden)]
1178 pub tasks: CT,
1179
1180 #[doc(hidden)]
1182 pub bridges: CB,
1183
1184 #[doc(hidden)]
1186 pub resources: ResourceManager,
1187
1188 #[cfg(feature = "std")]
1191 #[doc(hidden)]
1192 pub thread_pools: Vec<Option<Arc<ThreadPool>>>,
1193
1194 #[doc(hidden)]
1196 pub monitor: M,
1197
1198 #[cfg(feature = "std")]
1204 #[doc(hidden)]
1205 pub execution_probe: ExecutionProbeHandle,
1206 #[cfg(not(feature = "std"))]
1207 #[doc(hidden)]
1208 pub execution_probe: RuntimeExecutionProbe,
1209
1210 #[doc(hidden)]
1212 pub copperlists_manager: CopperListsManager<P, NBCL>,
1213
1214 #[doc(hidden)]
1216 pub keyframes_manager: KeyFramesManager,
1217
1218 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1220 #[doc(hidden)]
1221 pub parallel_rt: ParallelRt<NBCL>,
1222
1223 #[doc(hidden)]
1225 pub runtime_config: RuntimeConfig,
1226}
1227
1228impl<
1230 CT,
1231 CB,
1232 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload,
1233 M: CuMonitor,
1234 const NBCL: usize,
1235> ClockProvider for CuRuntime<CT, CB, P, M, NBCL>
1236{
1237 fn get_clock(&self) -> RobotClock {
1238 self.clock.clone()
1239 }
1240}
1241
1242impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> CuRuntime<CT, CB, P, M, NBCL> {
1243 #[inline]
1245 pub fn clock(&self) -> RobotClock {
1246 self.clock.clone()
1247 }
1248
1249 #[doc(hidden)]
1251 #[inline]
1252 pub fn clock_ref(&self) -> &RobotClock {
1253 &self.clock
1254 }
1255
1256 #[inline]
1258 pub fn subsystem_code(&self) -> u16 {
1259 self.subsystem_code
1260 }
1261
1262 #[inline]
1264 pub fn instance_id(&self) -> u32 {
1265 self.instance_id
1266 }
1267}
1268
1269#[cfg(feature = "std")]
1270impl<
1271 'cfg,
1272 CT,
1273 CB,
1274 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1275 M: CuMonitor,
1276 const NBCL: usize,
1277 TI,
1278 BI,
1279 MI,
1280 CLW,
1281 KFW,
1282> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLW, KFW>
1283where
1284 TI: for<'c> Fn(
1285 Vec<Option<&'c ComponentConfig>>,
1286 &mut ResourceManager,
1287 &[Option<Arc<ThreadPool>>],
1288 ) -> CuResult<CT>,
1289 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1290 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1291 CLW: WriteStream<CopperList<P>> + 'static,
1292 KFW: WriteStream<KeyFrame> + 'static,
1293{
1294 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1295 let Self {
1296 clock,
1297 config,
1298 mission,
1299 subsystem,
1300 instance_id,
1301 resources,
1302 thread_pools,
1303 parts,
1304 copperlists_logger,
1305 keyframes_logger,
1306 } = self;
1307 let mut resources =
1308 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1309 let thread_pools = thread_pools.unwrap_or_default();
1310
1311 let graph = config.get_graph(Some(mission))?;
1312 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1313 .get_all_nodes()
1314 .iter()
1315 .map(|(_, node)| node.get_instance_config())
1316 .collect();
1317
1318 let tasks =
1319 (parts.tasks_instanciator)(all_instances_configs, &mut resources, &thread_pools)?;
1320
1321 #[cfg(feature = "std")]
1322 let execution_probe = std::sync::Arc::new(RuntimeExecutionProbe::default());
1323 #[cfg(not(feature = "std"))]
1324 let execution_probe = RuntimeExecutionProbe::default();
1325 let monitor_metadata = CuMonitoringMetadata::new(
1326 CompactString::from(mission),
1327 parts.monitored_components,
1328 parts.culist_component_mapping,
1329 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1330 build_monitor_topology(config, mission)?,
1331 None,
1332 )?
1333 .with_subsystem_id(subsystem.id())
1334 .with_instance_id(instance_id);
1335 #[cfg(feature = "std")]
1336 let monitor_runtime =
1337 CuMonitoringRuntime::new(MonitorExecutionProbe::from_shared(execution_probe.clone()));
1338 #[cfg(not(feature = "std"))]
1339 let monitor_runtime = CuMonitoringRuntime::unavailable();
1340 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1341 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1342
1343 let (copperlists_logger, keyframes_logger, keyframe_interval) = match &config.logging {
1344 Some(logging_config) if logging_config.enable_task_logging => (
1345 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1346 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1347 logging_config.keyframe_interval.unwrap(),
1348 ),
1349 Some(_) => (None, None, 0),
1350 None => (
1351 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1352 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1353 DEFAULT_KEYFRAME_INTERVAL,
1354 ),
1355 };
1356
1357 let copperlists_manager = CopperListsManager::new(copperlists_logger)?;
1358 #[cfg(target_os = "none")]
1359 {
1360 let cl_size = core::mem::size_of::<CopperList<P>>();
1361 let total_bytes = cl_size.saturating_mul(NBCL);
1362 info!(
1363 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1364 NBCL, cl_size, total_bytes
1365 );
1366 }
1367
1368 let keyframes_manager = KeyFramesManager {
1369 inner: KeyFrame::new(),
1370 logger: keyframes_logger,
1371 keyframe_interval,
1372 last_encoded_bytes: 0,
1373 forced_timestamp: None,
1374 locked: false,
1375 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1376 };
1377 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1378 let parallel_rt = ParallelRt::new(parts.parallel_rt_metadata)?;
1379
1380 let runtime_config = config.runtime.clone().unwrap_or_default();
1381 runtime_config.validate()?;
1382
1383 Ok(CuRuntime {
1384 subsystem_code: subsystem.code(),
1385 instance_id,
1386 tasks,
1387 bridges,
1388 resources,
1389 thread_pools,
1390 monitor,
1391 execution_probe,
1392 clock,
1393 copperlists_manager,
1394 keyframes_manager,
1395 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1396 parallel_rt,
1397 runtime_config,
1398 })
1399 }
1400}
1401
1402#[cfg(not(feature = "std"))]
1403impl<
1404 'cfg,
1405 CT,
1406 CB,
1407 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1408 M: CuMonitor,
1409 const NBCL: usize,
1410 TI,
1411 BI,
1412 MI,
1413 CLW,
1414 KFW,
1415> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLW, KFW>
1416where
1417 TI: for<'c> Fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>,
1418 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1419 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1420 CLW: WriteStream<CopperList<P>> + 'static,
1421 KFW: WriteStream<KeyFrame> + 'static,
1422{
1423 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1424 let Self {
1425 clock,
1426 config,
1427 mission,
1428 subsystem,
1429 instance_id,
1430 resources,
1431 parts,
1432 copperlists_logger,
1433 keyframes_logger,
1434 } = self;
1435 let mut resources =
1436 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1437
1438 let graph = config.get_graph(Some(mission))?;
1439 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1440 .get_all_nodes()
1441 .iter()
1442 .map(|(_, node)| node.get_instance_config())
1443 .collect();
1444
1445 let tasks = (parts.tasks_instanciator)(all_instances_configs, &mut resources)?;
1446
1447 let execution_probe = RuntimeExecutionProbe::default();
1448 let monitor_metadata = CuMonitoringMetadata::new(
1449 CompactString::from(mission),
1450 parts.monitored_components,
1451 parts.culist_component_mapping,
1452 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1453 build_monitor_topology(config, mission)?,
1454 None,
1455 )?
1456 .with_subsystem_id(subsystem.id())
1457 .with_instance_id(instance_id);
1458 let monitor_runtime = CuMonitoringRuntime::unavailable();
1459 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1460 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1461
1462 let (copperlists_logger, keyframes_logger, keyframe_interval) = match &config.logging {
1463 Some(logging_config) if logging_config.enable_task_logging => (
1464 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1465 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1466 logging_config.keyframe_interval.unwrap(),
1467 ),
1468 Some(_) => (None, None, 0),
1469 None => (
1470 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1471 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1472 DEFAULT_KEYFRAME_INTERVAL,
1473 ),
1474 };
1475
1476 let copperlists_manager = CopperListsManager::new(copperlists_logger)?;
1477 #[cfg(target_os = "none")]
1478 {
1479 let cl_size = core::mem::size_of::<CopperList<P>>();
1480 let total_bytes = cl_size.saturating_mul(NBCL);
1481 info!(
1482 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1483 NBCL, cl_size, total_bytes
1484 );
1485 }
1486
1487 let keyframes_manager = KeyFramesManager {
1488 inner: KeyFrame::new(),
1489 logger: keyframes_logger,
1490 keyframe_interval,
1491 last_encoded_bytes: 0,
1492 forced_timestamp: None,
1493 locked: false,
1494 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1495 };
1496
1497 let runtime_config = config.runtime.clone().unwrap_or_default();
1498 runtime_config.validate()?;
1499
1500 Ok(CuRuntime {
1501 subsystem_code: subsystem.code(),
1502 instance_id,
1503 tasks,
1504 bridges,
1505 resources,
1506 monitor,
1507 execution_probe,
1508 clock,
1509 copperlists_manager,
1510 keyframes_manager,
1511 runtime_config,
1512 })
1513 }
1514}
1515
1516#[derive(Clone, Encode, Decode)]
1521pub struct KeyFrame {
1522 pub culistid: u64,
1524 pub timestamp: CuTime,
1526 pub serialized_tasks: Vec<u8>,
1528}
1529
1530impl KeyFrame {
1531 fn new() -> Self {
1532 KeyFrame {
1533 culistid: 0,
1534 timestamp: CuTime::default(),
1535 serialized_tasks: KEYFRAME_PAYLOAD_HEADER.to_vec(),
1536 }
1537 }
1538
1539 fn reset(&mut self, culistid: u64, timestamp: CuTime) {
1541 self.culistid = culistid;
1542 self.timestamp = timestamp;
1543 self.serialized_tasks.clear();
1544 self.serialized_tasks
1545 .extend_from_slice(KEYFRAME_PAYLOAD_HEADER);
1546 }
1547
1548 fn add_frozen_task(&mut self, task: &impl Freezable) -> Result<usize, EncodeError> {
1550 let cfg = bincode::config::standard();
1551 let start = self.serialized_tasks.len();
1552 let payload_offset =
1553 start
1554 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
1555 .ok_or(EncodeError::Other(
1556 "keyframe component frame offset overflow",
1557 ))?;
1558 if payload_offset > self.serialized_tasks.capacity() {
1559 return Err(EncodeError::UnexpectedEnd);
1560 }
1561
1562 self.serialized_tasks.resize(payload_offset, 0);
1563 let length_offset = start;
1564 self.serialized_tasks[length_offset..payload_offset].fill(0);
1565
1566 let mut encoder =
1567 EncoderImpl::<_, _>::new(PreallocatedVecWriter(&mut self.serialized_tasks), cfg);
1568 if let Err(error) = BincodeAdapter(task).encode(&mut encoder) {
1569 self.serialized_tasks.truncate(start);
1570 return Err(error);
1571 }
1572 let payload_len = encoder.into_writer().0.len() - payload_offset;
1573 let payload_len = u32::try_from(payload_len).map_err(|_| {
1574 self.serialized_tasks.truncate(start);
1575 EncodeError::OtherString(
1576 "keyframe component snapshot exceeds the u32 frame limit".to_string(),
1577 )
1578 })?;
1579 self.serialized_tasks
1580 .truncate(payload_offset + payload_len as usize);
1581 self.serialized_tasks[length_offset..payload_offset]
1582 .copy_from_slice(&payload_len.to_le_bytes());
1583 Ok(self.serialized_tasks.len() - start)
1584 }
1585}
1586
1587const KEYFRAME_PAYLOAD_MAGIC: &[u8; 4] = b"CUKF";
1588const KEYFRAME_PAYLOAD_VERSION: u8 = 1;
1589const KEYFRAME_PAYLOAD_HEADER: &[u8; 5] = b"CUKF\x01";
1590const KEYFRAME_FRAME_HEADER_LEN: usize = 4;
1591
1592#[doc(hidden)]
1594pub struct KeyFramePayloadReader<'a> {
1595 remaining: &'a [u8],
1596}
1597
1598impl<'a> KeyFramePayloadReader<'a> {
1599 pub fn new(keyframe: &'a KeyFrame) -> CuResult<Self> {
1601 let payload = keyframe.serialized_tasks.as_slice();
1602 if payload.len() < KEYFRAME_PAYLOAD_HEADER.len()
1603 || payload[..KEYFRAME_PAYLOAD_MAGIC.len()] != *KEYFRAME_PAYLOAD_MAGIC
1604 {
1605 return Err(CuError::from(
1606 "Unsupported legacy keyframe payload: expected framed format version 1",
1607 ));
1608 }
1609 let version = payload[KEYFRAME_PAYLOAD_MAGIC.len()];
1610 if version != KEYFRAME_PAYLOAD_VERSION {
1611 return Err(CuError::from(format!(
1612 "Unsupported keyframe payload version {version}; expected {KEYFRAME_PAYLOAD_VERSION}"
1613 )));
1614 }
1615 Ok(Self {
1616 remaining: &payload[KEYFRAME_PAYLOAD_HEADER.len()..],
1617 })
1618 }
1619
1620 pub fn next_frame(&mut self) -> CuResult<&'a [u8]> {
1622 if self.remaining.len() < KEYFRAME_FRAME_HEADER_LEN {
1623 return Err(CuError::from("Keyframe ended before next component frame"));
1624 }
1625 let payload_len = u32::from_le_bytes(
1626 self.remaining[..KEYFRAME_FRAME_HEADER_LEN]
1627 .try_into()
1628 .map_err(|_| CuError::from("Invalid keyframe component frame length"))?,
1629 ) as usize;
1630 let frame_end = KEYFRAME_FRAME_HEADER_LEN
1631 .checked_add(payload_len)
1632 .ok_or_else(|| CuError::from("Keyframe component frame length overflow"))?;
1633 if frame_end > self.remaining.len() {
1634 return Err(CuError::from("Keyframe component frame is truncated"));
1635 }
1636 let payload = &self.remaining[KEYFRAME_FRAME_HEADER_LEN..frame_end];
1637 self.remaining = &self.remaining[frame_end..];
1638 Ok(payload)
1639 }
1640
1641 pub fn finish(self) -> CuResult<()> {
1643 if self.remaining.is_empty() {
1644 Ok(())
1645 } else {
1646 Err(CuError::from("Keyframe contains trailing component data"))
1647 }
1648 }
1649}
1650
1651struct FrameSliceReader<'a> {
1652 remaining: &'a [u8],
1653}
1654
1655impl Reader for FrameSliceReader<'_> {
1656 fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> {
1657 if bytes.len() > self.remaining.len() {
1658 return Err(DecodeError::UnexpectedEnd {
1659 additional: bytes.len() - self.remaining.len(),
1660 });
1661 }
1662 let (read, remaining) = self.remaining.split_at(bytes.len());
1663 bytes.copy_from_slice(read);
1664 self.remaining = remaining;
1665 Ok(())
1666 }
1667
1668 fn peek_read(&mut self, length: usize) -> Option<&[u8]> {
1669 self.remaining.get(..length)
1670 }
1671
1672 fn consume(&mut self, length: usize) {
1673 self.remaining = self.remaining.get(length..).unwrap_or_default();
1674 }
1675}
1676
1677#[doc(hidden)]
1679pub fn thaw_keyframe_component(item: &mut impl Freezable, frame: &[u8]) -> CuResult<()> {
1680 let reader = FrameSliceReader { remaining: frame };
1681 let mut decoder = DecoderImpl::new(reader, bincode::config::standard(), ());
1682 item.thaw(&mut decoder)
1683 .map_err(|error| CuError::from(format!("Failed to thaw keyframe component: {error}")))?;
1684 let trailing = decoder.reader().remaining.len();
1685 if trailing != 0 {
1686 return Err(CuError::from(format!(
1687 "Keyframe component snapshot has {} trailing bytes",
1688 trailing
1689 )));
1690 }
1691 Ok(())
1692}
1693
1694#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1696pub enum RuntimeLifecycleConfigSource {
1697 ProgrammaticOverride,
1698 ExternalFile,
1699 BundledDefault,
1700}
1701
1702#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1704pub struct RuntimeLifecycleStackInfo {
1705 pub app_name: String,
1706 pub app_version: String,
1707 pub git_commit: Option<String>,
1708 pub git_dirty: Option<bool>,
1709 pub subsystem_id: Option<String>,
1710 pub subsystem_code: u16,
1711 pub instance_id: u32,
1712}
1713
1714#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1716pub enum RuntimeLifecycleEvent {
1717 Instantiated {
1718 config_source: RuntimeLifecycleConfigSource,
1719 effective_config_ron: String,
1720 stack: RuntimeLifecycleStackInfo,
1721 },
1722 MissionStarted {
1723 mission: String,
1724 },
1725 MissionStopped {
1726 mission: String,
1727 reason: String,
1730 },
1731 Panic {
1733 message: String,
1734 file: Option<String>,
1735 line: Option<u32>,
1736 column: Option<u32>,
1737 },
1738 ShutdownCompleted,
1739}
1740
1741#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1743pub struct RuntimeLifecycleRecord {
1744 pub timestamp: CuTime,
1745 pub event: RuntimeLifecycleEvent,
1746}
1747
1748impl<
1749 CT,
1750 CB,
1751 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1752 M: CuMonitor,
1753 const NBCL: usize,
1754> CuRuntime<CT, CB, P, M, NBCL>
1755{
1756 #[inline]
1760 pub fn record_execution_marker(&self, marker: ExecutionMarker) {
1761 self.execution_probe.record(marker);
1762 }
1763
1764 #[inline]
1769 pub fn execution_probe_ref(&self) -> &RuntimeExecutionProbe {
1770 #[cfg(feature = "std")]
1771 {
1772 self.execution_probe.as_ref()
1773 }
1774
1775 #[cfg(not(feature = "std"))]
1776 {
1777 &self.execution_probe
1778 }
1779 }
1780}
1781
1782#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1787pub enum CuTaskType {
1788 Source,
1789 Regular,
1790 Sink,
1791}
1792
1793impl From<TaskKind> for CuTaskType {
1794 fn from(value: TaskKind) -> Self {
1795 match value {
1796 TaskKind::Source => CuTaskType::Source,
1797 TaskKind::Regular => CuTaskType::Regular,
1798 TaskKind::Sink => CuTaskType::Sink,
1799 }
1800 }
1801}
1802
1803#[derive(Debug, Clone)]
1804pub struct CuOutputPack {
1805 pub culist_index: u32,
1806 pub msg_types: Vec<String>,
1807}
1808
1809#[derive(Debug, Clone)]
1810pub struct CuInputMsg {
1811 pub culist_index: u32,
1812 pub msg_type: String,
1813 pub src_port: usize,
1814 pub edge_id: usize,
1815 pub connection_order: usize,
1816}
1817
1818#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1826pub enum CuStepPhase {
1827 #[default]
1829 Whole,
1830 AnytimeBase,
1833 AnytimeRefine,
1835}
1836
1837pub struct CuExecutionStep {
1839 pub node_id: NodeId,
1841 pub node: Node,
1843 pub task_type: CuTaskType,
1845 pub phase: CuStepPhase,
1848
1849 pub input_msg_indices_types: Vec<CuInputMsg>,
1852
1853 pub output_msg_pack: Option<CuOutputPack>,
1856}
1857
1858impl Debug for CuExecutionStep {
1859 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1860 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
1861 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
1862 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
1863 f.write_str(format!(" phase: {:?}\n", self.phase).as_str())?;
1864 f.write_str(
1865 format!(
1866 " input_msg_types: {:?}\n",
1867 self.input_msg_indices_types
1868 )
1869 .as_str(),
1870 )?;
1871 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
1872 Ok(())
1873 }
1874}
1875
1876pub struct CuExecutionLoop {
1881 pub steps: Vec<CuExecutionUnit>,
1882 pub loop_count: Option<u32>,
1883}
1884
1885impl Debug for CuExecutionLoop {
1886 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1887 f.write_str("CuExecutionLoop:\n")?;
1888 for step in &self.steps {
1889 match step {
1890 CuExecutionUnit::Step(step) => {
1891 step.fmt(f)?;
1892 }
1893 CuExecutionUnit::Loop(l) => {
1894 l.fmt(f)?;
1895 }
1896 }
1897 }
1898
1899 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
1900 Ok(())
1901 }
1902}
1903
1904#[derive(Debug)]
1906pub enum CuExecutionUnit {
1907 Step(Box<CuExecutionStep>),
1908 Loop(CuExecutionLoop),
1909}
1910
1911fn find_output_pack_from_nodeid(
1912 node_id: NodeId,
1913 steps: &Vec<CuExecutionUnit>,
1914) -> Option<CuOutputPack> {
1915 for step in steps {
1916 match step {
1917 CuExecutionUnit::Loop(loop_unit) => {
1918 if let Some(output_pack) = find_output_pack_from_nodeid(node_id, &loop_unit.steps) {
1919 return Some(output_pack);
1920 }
1921 }
1922 CuExecutionUnit::Step(step) if step.node_id == node_id => {
1923 return step.output_msg_pack.clone();
1924 }
1925 _ => {}
1926 }
1927 }
1928 None
1929}
1930
1931pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
1932 let node = graph
1933 .get_node(node_id)
1934 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
1935
1936 if node.get_flavor() == crate::config::Flavor::Task {
1937 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
1938 }
1939
1940 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
1941 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
1942 Ok(match (has_inputs, has_outputs) {
1943 (false, true) => CuTaskType::Source,
1944 (true, false) => CuTaskType::Sink,
1945 _ => CuTaskType::Regular,
1946 })
1947}
1948
1949fn sort_inputs_by_connection_order(input_msg_indices_types: &mut [CuInputMsg]) {
1954 input_msg_indices_types.sort_by_key(|input| input.connection_order);
1955}
1956
1957fn plan_tasks_tree_branch(
1959 graph: &CuGraph,
1960 mut next_culist_output_index: u32,
1961 starting_point: NodeId,
1962 plan: &mut Vec<CuExecutionUnit>,
1963) -> CuResult<(u32, bool)> {
1964 #[cfg(all(feature = "std", feature = "macro_debug"))]
1965 eprintln!("-- starting branch from node {starting_point}");
1966
1967 let mut handled = false;
1968
1969 for id in graph.bfs_nodes(starting_point) {
1970 let node_ref = graph.get_node(id).unwrap();
1971 #[cfg(all(feature = "std", feature = "macro_debug"))]
1972 eprintln!(" Visiting node: {node_ref:?}");
1973
1974 let mut input_msg_indices_types: Vec<CuInputMsg> = Vec::new();
1975 let output_msg_pack: Option<CuOutputPack>;
1976 let task_type = find_task_type_for_id(graph, id)?;
1977
1978 match task_type {
1979 CuTaskType::Source => {
1980 #[cfg(all(feature = "std", feature = "macro_debug"))]
1981 eprintln!(" → Source node, assign output index {next_culist_output_index}");
1982 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1983 if msg_types.is_empty() {
1984 return Err(CuError::from(format!(
1985 "Source node '{}' has no declared outputs",
1986 node_ref.get_id()
1987 )));
1988 }
1989 output_msg_pack = Some(CuOutputPack {
1990 culist_index: next_culist_output_index,
1991 msg_types,
1992 });
1993 next_culist_output_index += 1;
1994 }
1995 CuTaskType::Sink => {
1996 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1997 edge_ids.sort();
1998 #[cfg(all(feature = "std", feature = "macro_debug"))]
1999 eprintln!(" → Sink with incoming edges: {edge_ids:?}");
2000 for edge_id in edge_ids {
2001 let edge = graph
2002 .edge(edge_id)
2003 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
2004 let pid = graph
2005 .get_node_id_by_name(edge.src.as_str())
2006 .unwrap_or_else(|| {
2007 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
2008 });
2009 let output_pack = find_output_pack_from_nodeid(pid, plan);
2010 if let Some(output_pack) = output_pack {
2011 #[cfg(all(feature = "std", feature = "macro_debug"))]
2012 eprintln!(" ✓ Input from {pid} ready: {output_pack:?}");
2013 let msg_type = edge.msg.as_str();
2014 let src_port = output_pack
2015 .msg_types
2016 .iter()
2017 .position(|msg| msg == msg_type)
2018 .unwrap_or_else(|| {
2019 panic!(
2020 "Missing output port for message type '{msg_type}' on node {pid}"
2021 )
2022 });
2023 input_msg_indices_types.push(CuInputMsg {
2024 culist_index: output_pack.culist_index,
2025 msg_type: msg_type.to_string(),
2026 src_port,
2027 edge_id,
2028 connection_order: edge.order,
2029 });
2030 } else {
2031 #[cfg(all(feature = "std", feature = "macro_debug"))]
2032 eprintln!(" ✗ Input from {pid} not ready, returning");
2033 return Ok((next_culist_output_index, handled));
2034 }
2035 }
2036 output_msg_pack = Some(CuOutputPack {
2037 culist_index: next_culist_output_index,
2038 msg_types: Vec::from(["()".to_string()]),
2039 });
2040 next_culist_output_index += 1;
2041 }
2042 CuTaskType::Regular => {
2043 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
2044 edge_ids.sort();
2045 #[cfg(all(feature = "std", feature = "macro_debug"))]
2046 eprintln!(" → Regular task with incoming edges: {edge_ids:?}");
2047 for edge_id in edge_ids {
2048 let edge = graph
2049 .edge(edge_id)
2050 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
2051 let pid = graph
2052 .get_node_id_by_name(edge.src.as_str())
2053 .unwrap_or_else(|| {
2054 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
2055 });
2056 let output_pack = find_output_pack_from_nodeid(pid, plan);
2057 if let Some(output_pack) = output_pack {
2058 #[cfg(all(feature = "std", feature = "macro_debug"))]
2059 eprintln!(" ✓ Input from {pid} ready: {output_pack:?}");
2060 let msg_type = edge.msg.as_str();
2061 let src_port = output_pack
2062 .msg_types
2063 .iter()
2064 .position(|msg| msg == msg_type)
2065 .unwrap_or_else(|| {
2066 panic!(
2067 "Missing output port for message type '{msg_type}' on node {pid}"
2068 )
2069 });
2070 input_msg_indices_types.push(CuInputMsg {
2071 culist_index: output_pack.culist_index,
2072 msg_type: msg_type.to_string(),
2073 src_port,
2074 edge_id,
2075 connection_order: edge.order,
2076 });
2077 } else {
2078 #[cfg(all(feature = "std", feature = "macro_debug"))]
2079 eprintln!(" ✗ Input from {pid} not ready, returning");
2080 return Ok((next_culist_output_index, handled));
2081 }
2082 }
2083 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
2084 if msg_types.is_empty() {
2085 return Err(CuError::from(format!(
2086 "Regular node '{}' has no declared outputs",
2087 node_ref.get_id()
2088 )));
2089 }
2090 output_msg_pack = Some(CuOutputPack {
2091 culist_index: next_culist_output_index,
2092 msg_types,
2093 });
2094 next_culist_output_index += 1;
2095 }
2096 }
2097
2098 sort_inputs_by_connection_order(&mut input_msg_indices_types);
2099
2100 if let Some(pos) = plan
2101 .iter()
2102 .position(|step| matches!(step, CuExecutionUnit::Step(s) if s.node_id == id))
2103 {
2104 #[cfg(all(feature = "std", feature = "macro_debug"))]
2105 eprintln!(" → Already in plan, modifying existing step");
2106 let mut step = plan.remove(pos);
2107 if let CuExecutionUnit::Step(ref mut s) = step {
2108 s.input_msg_indices_types = input_msg_indices_types;
2109 }
2110 plan.push(step);
2111 } else {
2112 #[cfg(all(feature = "std", feature = "macro_debug"))]
2113 eprintln!(" → New step added to plan");
2114 let step = CuExecutionStep {
2115 node_id: id,
2116 node: node_ref.clone(),
2117 task_type,
2118 phase: CuStepPhase::default(),
2119 input_msg_indices_types,
2120 output_msg_pack,
2121 };
2122 plan.push(CuExecutionUnit::Step(Box::new(step)));
2123 }
2124
2125 handled = true;
2126 }
2127
2128 #[cfg(all(feature = "std", feature = "macro_debug"))]
2129 eprintln!("-- finished branch from node {starting_point} with handled={handled}");
2130 Ok((next_culist_output_index, handled))
2131}
2132
2133pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
2136 #[cfg(all(feature = "std", feature = "macro_debug"))]
2137 eprintln!("[runtime plan]");
2138 let mut plan = Vec::new();
2139 let mut next_culist_output_index = 0u32;
2140
2141 let mut queue: VecDeque<NodeId> = VecDeque::new();
2142 for node_id in graph.node_ids() {
2143 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
2144 queue.push_back(node_id);
2145 }
2146 }
2147
2148 #[cfg(all(feature = "std", feature = "macro_debug"))]
2149 eprintln!("Initial source nodes: {queue:?}");
2150
2151 while let Some(start_node) = queue.pop_front() {
2152 #[cfg(all(feature = "std", feature = "macro_debug"))]
2153 eprintln!("→ Starting BFS from source {start_node}");
2154 for node_id in graph.bfs_nodes(start_node) {
2155 let already_in_plan = plan
2156 .iter()
2157 .any(|unit| matches!(unit, CuExecutionUnit::Step(s) if s.node_id == node_id));
2158 if already_in_plan {
2159 #[cfg(all(feature = "std", feature = "macro_debug"))]
2160 eprintln!(" → Node {node_id} already planned, skipping");
2161 continue;
2162 }
2163
2164 #[cfg(all(feature = "std", feature = "macro_debug"))]
2165 eprintln!(" Planning from node {node_id}");
2166 let (new_index, handled) =
2167 plan_tasks_tree_branch(graph, next_culist_output_index, node_id, &mut plan)?;
2168 next_culist_output_index = new_index;
2169
2170 if !handled {
2171 #[cfg(all(feature = "std", feature = "macro_debug"))]
2172 eprintln!(" ✗ Node {node_id} was not handled, skipping enqueue of neighbors");
2173 continue;
2174 }
2175
2176 #[cfg(all(feature = "std", feature = "macro_debug"))]
2177 eprintln!(" ✓ Node {node_id} handled successfully, enqueueing neighbors");
2178 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
2179 #[cfg(all(feature = "std", feature = "macro_debug"))]
2180 eprintln!(" → Enqueueing neighbor {neighbor}");
2181 queue.push_back(neighbor);
2182 }
2183 }
2184 }
2185
2186 let mut planned_nodes = BTreeSet::new();
2187 for unit in &plan {
2188 if let CuExecutionUnit::Step(step) = unit {
2189 planned_nodes.insert(step.node_id);
2190 }
2191 }
2192
2193 let mut missing = Vec::new();
2194 for node_id in graph.node_ids() {
2195 if !planned_nodes.contains(&node_id) {
2196 if let Some(node) = graph.get_node(node_id) {
2197 missing.push(node.get_id().to_string());
2198 } else {
2199 missing.push(format!("node_id_{node_id}"));
2200 }
2201 }
2202 }
2203
2204 if !missing.is_empty() {
2205 missing.sort();
2206 return Err(CuError::from(format!(
2207 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
2208 missing.join(", ")
2209 )));
2210 }
2211
2212 Ok(CuExecutionLoop {
2213 steps: plan,
2214 loop_count: None,
2215 })
2216}
2217
2218pub fn expand_anytime_steps(plan: &mut CuExecutionLoop) -> CuResult<()> {
2236 loop {
2237 let Some(base_pos) = plan.steps.iter().position(|unit| {
2240 matches!(
2241 unit,
2242 CuExecutionUnit::Step(step) if step.phase == CuStepPhase::Whole
2243 && step.node.anytime().is_some()
2244 && !step.node.is_background()
2245 )
2246 }) else {
2247 return Ok(());
2248 };
2249
2250 let CuExecutionUnit::Step(base_step) = &mut plan.steps[base_pos] else {
2251 unreachable!("position() only matches steps");
2252 };
2253 let anytime = base_step
2254 .node
2255 .anytime()
2256 .expect("position() only matches anytime nodes");
2257 let Some(max_refines) = anytime.max_refines else {
2260 return Err(CuError::from(format!(
2261 "Task '{}': a foreground anytime task needs anytime.max_refines to expand into a static plan.",
2262 base_step.node.get_id()
2263 )));
2264 };
2265 base_step.phase = CuStepPhase::AnytimeBase;
2266 let output_pack = base_step.output_msg_pack.clone().ok_or_else(|| {
2267 CuError::from(format!(
2268 "Task '{}': an anytime task needs an output to refine.",
2269 base_step.node.get_id()
2270 ))
2271 })?;
2272 let output_index = output_pack.culist_index;
2273 let node_id = base_step.node_id;
2274 let node = base_step.node.clone();
2275 let task_type = base_step.task_type;
2276
2277 let refine_step = || {
2278 CuExecutionUnit::Step(Box::new(CuExecutionStep {
2279 node_id,
2280 node: node.clone(),
2281 task_type,
2282 phase: CuStepPhase::AnytimeRefine,
2283 input_msg_indices_types: Vec::new(),
2284 output_msg_pack: Some(output_pack.clone()),
2285 }))
2286 };
2287
2288 let consumer_pos = plan.steps[base_pos + 1..]
2291 .iter()
2292 .position(|unit| {
2293 matches!(
2294 unit,
2295 CuExecutionUnit::Step(step) if step
2296 .input_msg_indices_types
2297 .iter()
2298 .any(|input| input.culist_index == output_index)
2299 )
2300 })
2301 .map(|offset| base_pos + 1 + offset)
2302 .unwrap_or(base_pos + 1);
2303
2304 let mut tail = plan.steps.split_off(base_pos + 1);
2305 let suffix = tail.split_off(consumer_pos - base_pos - 1);
2306 let gap = tail;
2307
2308 let mut remaining = max_refines.max(1);
2310 remaining -= 1;
2311 plan.steps.push(refine_step());
2312 for gap_unit in gap {
2313 plan.steps.push(gap_unit);
2314 if remaining > 0 {
2315 remaining -= 1;
2316 plan.steps.push(refine_step());
2317 }
2318 }
2319 for _ in 0..remaining {
2320 plan.steps.push(refine_step());
2321 }
2322 plan.steps.extend(suffix);
2323 }
2324}
2325
2326#[cfg(test)]
2328mod tests {
2329 use super::*;
2330 use crate::config::Node;
2331 use crate::context::CuContext;
2332 use crate::cutask::CuSinkTask;
2333 use crate::cutask::{CuSrcTask, Freezable};
2334 use crate::monitoring::NoMonitor;
2335 use crate::reflect::Reflect;
2336 use bincode::Encode;
2337 use core::cell::Cell;
2338 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
2339 use serde_derive::{Deserialize, Serialize};
2340 #[cfg(feature = "std")]
2341 use std::sync::{Arc, Mutex};
2342
2343 struct CountingSnapshot<'a> {
2344 calls: &'a Cell<usize>,
2345 value: u32,
2346 fail: bool,
2347 }
2348
2349 impl Freezable for CountingSnapshot<'_> {
2350 fn freeze<E: bincode::enc::Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
2351 self.calls.set(self.calls.get() + 1);
2352 self.value.encode(encoder)?;
2353 if self.fail {
2354 Err(EncodeError::OtherString(
2355 "intentional freeze failure".to_string(),
2356 ))
2357 } else {
2358 Ok(())
2359 }
2360 }
2361 }
2362
2363 #[derive(Default)]
2364 struct SnapshotValue(u32);
2365
2366 impl Freezable for SnapshotValue {
2367 fn thaw<D: bincode::de::Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
2368 self.0 = u32::decode(decoder)?;
2369 Ok(())
2370 }
2371 }
2372
2373 #[test]
2374 fn keyframe_frames_freeze_once_and_roll_back_only_failed_frame() {
2375 let calls = Cell::new(0);
2376 let mut keyframe = KeyFrame::new();
2377 keyframe
2378 .serialized_tasks
2379 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
2380 .unwrap();
2381 keyframe.reset(7, CuTime::from_nanos(70));
2382 keyframe
2383 .add_frozen_task(&CountingSnapshot {
2384 calls: &calls,
2385 value: 11,
2386 fail: false,
2387 })
2388 .unwrap();
2389 let committed_len = keyframe.serialized_tasks.len();
2390
2391 let failing = CountingSnapshot {
2392 calls: &calls,
2393 value: 99,
2394 fail: true,
2395 };
2396 assert!(keyframe.add_frozen_task(&failing).is_err());
2397 assert_eq!(keyframe.serialized_tasks.len(), committed_len);
2398
2399 keyframe
2400 .add_frozen_task(&CountingSnapshot {
2401 calls: &calls,
2402 value: 22,
2403 fail: false,
2404 })
2405 .unwrap();
2406 assert_eq!(calls.get(), 3, "each append must call freeze exactly once");
2407 let first_payload_len = bincode::encode_to_vec(11u32, bincode::config::standard())
2408 .unwrap()
2409 .len();
2410 let second_payload_len = bincode::encode_to_vec(22u32, bincode::config::standard())
2411 .unwrap()
2412 .len();
2413 assert_eq!(
2414 keyframe.serialized_tasks.len(),
2415 KEYFRAME_PAYLOAD_HEADER.len()
2416 + 2 * KEYFRAME_FRAME_HEADER_LEN
2417 + first_payload_len
2418 + second_payload_len,
2419 "component frames carry only a length prefix"
2420 );
2421
2422 let mut frames = KeyFramePayloadReader::new(&keyframe).unwrap();
2423 let mut first = SnapshotValue::default();
2424 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
2425 let mut second = SnapshotValue::default();
2426 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
2427 frames.finish().unwrap();
2428 assert_eq!((first.0, second.0), (11, 22));
2429 }
2430
2431 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2432 #[test]
2433 fn preallocated_keyframe_append_does_not_allocate() {
2434 let calls = Cell::new(0);
2435 let mut keyframe = KeyFrame::new();
2436 keyframe
2437 .serialized_tasks
2438 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
2439 .unwrap();
2440 keyframe.reset(3, CuTime::from_nanos(30));
2441
2442 let allocations = crate::monitoring::ScopedAllocCounter::new();
2443 keyframe
2444 .add_frozen_task(&CountingSnapshot {
2445 calls: &calls,
2446 value: 42,
2447 fail: false,
2448 })
2449 .unwrap();
2450
2451 assert_eq!(allocations.allocated(), 0);
2452 assert_eq!(calls.get(), 1);
2453 }
2454
2455 #[test]
2456 fn keyframe_reader_rejects_legacy_payload_clearly() {
2457 let keyframe = KeyFrame {
2458 culistid: 0,
2459 timestamp: CuTime::default(),
2460 serialized_tasks: vec![0, 1, 2],
2461 };
2462 let error = match KeyFramePayloadReader::new(&keyframe) {
2463 Ok(_) => panic!("legacy payload unexpectedly accepted"),
2464 Err(error) => error,
2465 };
2466 assert!(error.to_string().contains("legacy keyframe payload"));
2467 }
2468
2469 #[derive(Reflect)]
2470 pub struct TestSource {}
2471
2472 impl Freezable for TestSource {}
2473
2474 impl CuSrcTask for TestSource {
2475 type Resources<'r> = ();
2476 type Output<'m> = ();
2477 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2478 where
2479 Self: Sized,
2480 {
2481 Ok(Self {})
2482 }
2483
2484 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
2485 Ok(())
2486 }
2487 }
2488
2489 #[derive(Reflect)]
2490 pub struct TestSink {}
2491
2492 impl Freezable for TestSink {}
2493
2494 impl CuSinkTask for TestSink {
2495 type Resources<'r> = ();
2496 type Input<'m> = ();
2497
2498 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2499 where
2500 Self: Sized,
2501 {
2502 Ok(Self {})
2503 }
2504
2505 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
2506 Ok(())
2507 }
2508 }
2509
2510 type Tasks = (TestSource, TestSink);
2512 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
2513 const TEST_NBCL: usize = 2;
2514
2515 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2516 struct Msgs(());
2517
2518 impl ErasedCuStampedDataSet for Msgs {
2519 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2520 Vec::new()
2521 }
2522 }
2523
2524 impl MatchingTasks for Msgs {
2525 fn get_all_task_ids() -> &'static [&'static str] {
2526 &[]
2527 }
2528 }
2529
2530 impl CuListZeroedInit for Msgs {
2531 fn init_zeroed(&mut self) {}
2532 }
2533
2534 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2535 struct IntMsgs(i32);
2536
2537 impl ErasedCuStampedDataSet for IntMsgs {
2538 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2539 Vec::new()
2540 }
2541 }
2542
2543 impl MatchingTasks for IntMsgs {
2544 fn get_all_task_ids() -> &'static [&'static str] {
2545 &[]
2546 }
2547 }
2548
2549 impl CuListZeroedInit for IntMsgs {
2550 fn init_zeroed(&mut self) {}
2551 }
2552
2553 #[cfg(feature = "std")]
2554 fn tasks_instanciator(
2555 all_instances_configs: Vec<Option<&ComponentConfig>>,
2556 _resources: &mut ResourceManager,
2557 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
2558 ) -> CuResult<Tasks> {
2559 Ok((
2560 TestSource::new(all_instances_configs[0], ())?,
2561 TestSink::new(all_instances_configs[1], ())?,
2562 ))
2563 }
2564
2565 #[cfg(not(feature = "std"))]
2566 fn tasks_instanciator(
2567 all_instances_configs: Vec<Option<&ComponentConfig>>,
2568 _resources: &mut ResourceManager,
2569 ) -> CuResult<Tasks> {
2570 Ok((
2571 TestSource::new(all_instances_configs[0], ())?,
2572 TestSink::new(all_instances_configs[1], ())?,
2573 ))
2574 }
2575
2576 fn monitor_instanciator(
2577 _config: &CuConfig,
2578 metadata: CuMonitoringMetadata,
2579 runtime: CuMonitoringRuntime,
2580 ) -> NoMonitor {
2581 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
2582 }
2583
2584 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
2585 Ok(())
2586 }
2587
2588 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
2589 Ok(ResourceManager::new(&[]))
2590 }
2591
2592 #[derive(Debug)]
2593 struct FakeWriter {}
2594
2595 impl<E: Encode> WriteStream<E> for FakeWriter {
2596 fn log(&mut self, _obj: &E) -> CuResult<()> {
2597 Ok(())
2598 }
2599 }
2600
2601 #[cfg(not(feature = "async-cl-io"))]
2602 #[derive(Debug)]
2603 struct RecordingSyncWriter {
2604 ids: Arc<Mutex<Vec<u64>>>,
2605 last_log_bytes: usize,
2606 fail_on: Option<u64>,
2607 }
2608
2609 #[cfg(not(feature = "async-cl-io"))]
2610 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
2611 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2612 self.ids.lock().unwrap().push(culist.id);
2613 if self.fail_on == Some(culist.id) {
2614 return Err(CuError::from(format!(
2615 "logger failed for CopperList #{}",
2616 culist.id
2617 )));
2618 }
2619 Ok(())
2620 }
2621
2622 fn last_log_bytes(&self) -> Option<usize> {
2623 Some(self.last_log_bytes)
2624 }
2625 }
2626
2627 #[test]
2628 fn test_runtime_instantiation() {
2629 let mut config = CuConfig::default();
2630 let graph = config.get_graph_mut(None).unwrap();
2631 graph.add_node(Node::new("a", "TestSource")).unwrap();
2632 graph.add_node(Node::new("b", "TestSink")).unwrap();
2633 graph.connect(0, 1, "()").unwrap();
2634 let runtime: CuResult<TestRuntime> =
2635 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2636 RobotClock::default(),
2637 &config,
2638 crate::config::DEFAULT_MISSION_ID,
2639 CuRuntimeParts::new(
2640 tasks_instanciator,
2641 &[],
2642 &[],
2643 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2644 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2645 monitor_instanciator,
2646 bridges_instanciator,
2647 ),
2648 FakeWriter {},
2649 FakeWriter {},
2650 )
2651 .try_with_resources_instantiator(resources_instanciator)
2652 .and_then(|builder| builder.build());
2653 assert!(runtime.is_ok());
2654 }
2655
2656 #[test]
2657 fn test_rate_target_period_rejects_zero() {
2658 let err = rate_target_period(0).expect_err("zero rate target should fail");
2659 assert!(
2660 err.to_string()
2661 .contains("Runtime rate target cannot be zero"),
2662 "unexpected error: {err}"
2663 );
2664 }
2665
2666 #[test]
2667 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
2668 let (clock, mock) = RobotClock::mock();
2669 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
2670 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
2671
2672 mock.set_value(10_000_000);
2673 limiter.mark_tick(&clock);
2674
2675 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
2676 }
2677
2678 #[test]
2679 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
2680 let (clock, mock) = RobotClock::mock();
2681 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
2682
2683 mock.set_value(35_000_000);
2684 limiter.mark_tick(&clock);
2685
2686 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
2687 }
2688
2689 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
2690 #[test]
2691 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
2692 let (clock, _) = RobotClock::mock();
2693 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
2694 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
2695
2696 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
2697 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
2698 }
2699
2700 #[cfg(not(feature = "async-cl-io"))]
2701 #[test]
2702 fn test_copperlists_manager_lifecycle() {
2703 let mut config = CuConfig::default();
2704 let graph = config.get_graph_mut(None).unwrap();
2705 graph.add_node(Node::new("a", "TestSource")).unwrap();
2706 graph.add_node(Node::new("b", "TestSink")).unwrap();
2707 graph.connect(0, 1, "()").unwrap();
2708
2709 let mut runtime: TestRuntime =
2710 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2711 RobotClock::default(),
2712 &config,
2713 crate::config::DEFAULT_MISSION_ID,
2714 CuRuntimeParts::new(
2715 tasks_instanciator,
2716 &[],
2717 &[],
2718 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2719 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2720 monitor_instanciator,
2721 bridges_instanciator,
2722 ),
2723 FakeWriter {},
2724 FakeWriter {},
2725 )
2726 .try_with_resources_instantiator(resources_instanciator)
2727 .and_then(|builder| builder.build())
2728 .unwrap();
2729
2730 {
2732 let copperlists = &mut runtime.copperlists_manager;
2733 let culist0 = copperlists
2734 .create()
2735 .expect("Ran out of space for copper lists");
2736 let id = culist0.id;
2737 assert_eq!(id, 0);
2738 culist0.change_state(CopperListState::Processing);
2739 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2740 }
2741
2742 {
2743 let copperlists = &mut runtime.copperlists_manager;
2744 let culist1 = copperlists
2745 .create()
2746 .expect("Ran out of space for copper lists");
2747 let id = culist1.id;
2748 assert_eq!(id, 1);
2749 culist1.change_state(CopperListState::Processing);
2750 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2751 }
2752
2753 {
2754 let copperlists = &mut runtime.copperlists_manager;
2755 let culist2 = copperlists.create();
2756 assert!(culist2.is_err());
2757 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2758 let _ = copperlists.end_of_processing(1);
2760 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2761 }
2762
2763 {
2765 let copperlists = &mut runtime.copperlists_manager;
2766 let culist2 = copperlists
2767 .create()
2768 .expect("Ran out of space for copper lists");
2769 let id = culist2.id;
2770 assert_eq!(id, 2);
2771 culist2.change_state(CopperListState::Processing);
2772 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2773 let _ = copperlists.end_of_processing(0);
2775 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2777
2778 let _ = copperlists.end_of_processing(2);
2780 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
2783 }
2784 }
2785
2786 #[cfg(not(feature = "async-cl-io"))]
2787 #[test]
2788 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
2789 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
2790
2791 assert_eq!(copperlists.next_cl_id(), 0);
2792 assert_eq!(copperlists.last_cl_id(), 0);
2793 assert!(copperlists.peek().is_none());
2794
2795 {
2796 let culist = copperlists.create().unwrap();
2797 culist.msgs.0 = 11;
2798 assert_eq!(culist.id, 0);
2799 assert_eq!(culist.get_state(), CopperListState::Initialized);
2800 }
2801
2802 assert_eq!(copperlists.next_cl_id(), 1);
2803 assert_eq!(copperlists.last_cl_id(), 0);
2804 let peeked = copperlists.peek().unwrap();
2805 assert_eq!(peeked.id, 0);
2806 assert_eq!(peeked.msgs.0, 11);
2807 assert_eq!(peeked.get_state(), CopperListState::Initialized);
2808 }
2809
2810 #[cfg(not(feature = "async-cl-io"))]
2811 #[test]
2812 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
2813 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2814
2815 {
2816 let culist = copperlists.create().unwrap();
2817 culist.msgs.0 = 41;
2818 culist.change_state(CopperListState::Processing);
2819 assert_eq!(culist.id, 0);
2820 }
2821
2822 copperlists.end_of_processing(0).unwrap();
2823 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2824
2825 let reused = copperlists.create().unwrap();
2826 assert_eq!(reused.id, 1);
2827 assert_eq!(reused.get_state(), CopperListState::Initialized);
2828 assert_eq!(reused.msgs.0, 41);
2829 }
2830
2831 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
2832 #[test]
2833 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
2834 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
2835 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
2836
2837 {
2838 let culist = copperlists.create().unwrap();
2839 culist.msgs.0 = 10;
2840 culist.change_state(CopperListState::Processing);
2841 }
2842 {
2843 let culist = copperlists.create().unwrap();
2844 culist.msgs.0 = 20;
2845 culist.change_state(CopperListState::Processing);
2846 }
2847
2848 let _ = copperlists.end_of_processing(99);
2849 }
2850
2851 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
2852 #[test]
2853 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
2854 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
2855 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2856
2857 {
2858 let culist = copperlists.create().unwrap();
2859 culist.msgs.0 = 10;
2860 assert_eq!(culist.get_state(), CopperListState::Initialized);
2861 }
2862
2863 let _ = copperlists.end_of_processing(0);
2864 }
2865
2866 #[cfg(not(feature = "async-cl-io"))]
2867 #[test]
2868 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
2869 let ids = Arc::new(Mutex::new(Vec::new()));
2870 let mut copperlists =
2871 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
2872 ids: ids.clone(),
2873 last_log_bytes: 17,
2874 fail_on: None,
2875 })))
2876 .unwrap();
2877
2878 {
2879 let culist = copperlists.create().unwrap();
2880 culist.msgs.0 = 10;
2881 culist.change_state(CopperListState::Processing);
2882 }
2883 {
2884 let culist = copperlists.create().unwrap();
2885 culist.msgs.0 = 20;
2886 culist.change_state(CopperListState::Processing);
2887 }
2888
2889 copperlists.end_of_processing(0).unwrap();
2890 assert!(ids.lock().unwrap().is_empty());
2891 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2892
2893 copperlists.end_of_processing(1).unwrap();
2894
2895 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
2896 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
2897 }
2898
2899 #[cfg(not(feature = "async-cl-io"))]
2900 #[test]
2901 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
2902 let ids = Arc::new(Mutex::new(Vec::new()));
2903 let mut copperlists =
2904 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
2905 ids: ids.clone(),
2906 last_log_bytes: 17,
2907 fail_on: None,
2908 })))
2909 .unwrap();
2910 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
2911
2912 {
2913 let culist = copperlists.create().unwrap();
2914 culist.msgs.0 = 10;
2915 culist.change_state(CopperListState::Processing);
2916 }
2917
2918 {
2919 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
2920 capture.select_slot(0);
2921 crate::monitoring::record_payload_handle_bytes(32);
2922 }
2923
2924 copperlists.end_of_processing(0).unwrap();
2925
2926 assert_eq!(*ids.lock().unwrap(), vec![0]);
2927 assert_eq!(copperlists.last_encoded_bytes, 17);
2928 assert_eq!(copperlists.last_handle_bytes, 32);
2929 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2930 }
2931
2932 #[cfg(not(feature = "async-cl-io"))]
2933 #[test]
2934 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
2935 let ids = Arc::new(Mutex::new(Vec::new()));
2936 let mut copperlists =
2937 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
2938 ids: ids.clone(),
2939 last_log_bytes: 17,
2940 fail_on: Some(0),
2941 })))
2942 .unwrap();
2943
2944 {
2945 let culist = copperlists.create().unwrap();
2946 culist.change_state(CopperListState::Processing);
2947 }
2948
2949 let err = copperlists.end_of_processing(0).unwrap_err();
2950
2951 assert!(
2952 err.to_string().contains("logger failed for CopperList #0"),
2953 "unexpected error: {err}"
2954 );
2955 assert_eq!(*ids.lock().unwrap(), vec![0]);
2956 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2957 assert_eq!(copperlists.last_encoded_bytes, 0);
2958 assert_eq!(copperlists.last_handle_bytes, 0);
2959
2960 let peeked = copperlists.peek().unwrap();
2961 assert_eq!(peeked.id, 0);
2962 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
2963 }
2964
2965 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
2966 #[test]
2967 #[should_panic(
2968 expected = "sync boxed end_of_processing expected CopperList #7 to be Processing"
2969 )]
2970 fn test_sync_end_of_processing_boxed_wrong_state_panics_in_debug() {
2971 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2972 let culist = Box::new(CopperList::new(7, IntMsgs::default()));
2973
2974 let _ = copperlists.end_of_processing_boxed(culist);
2975 }
2976
2977 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2978 #[derive(Debug, Default)]
2979 struct RecordingWriter {
2980 ids: Arc<Mutex<Vec<u64>>>,
2981 }
2982
2983 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2984 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
2985 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
2986 self.ids.lock().unwrap().push(culist.id);
2987 std::thread::sleep(std::time::Duration::from_millis(2));
2988 Ok(())
2989 }
2990 }
2991
2992 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2993 #[test]
2994 fn test_async_copperlists_manager_flushes_in_order() {
2995 let ids = Arc::new(Mutex::new(Vec::new()));
2996 let mut copperlists = CopperListsManager::<Msgs, 4>::new(Some(Box::new(RecordingWriter {
2997 ids: ids.clone(),
2998 })))
2999 .unwrap();
3000
3001 for expected_id in 0..4 {
3002 let culist = copperlists.create().unwrap();
3003 assert_eq!(culist.id, expected_id);
3004 culist.change_state(CopperListState::Processing);
3005 copperlists.end_of_processing(expected_id).unwrap();
3006 }
3007
3008 copperlists.finish_pending().unwrap();
3009 assert_eq!(copperlists.available_copper_lists().unwrap(), 4);
3010 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
3011 }
3012
3013 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3014 #[test]
3015 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
3016 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3017
3018 {
3019 let culist = copperlists.create().unwrap();
3020 assert_eq!(culist.id, 0);
3021 assert_eq!(culist.get_state(), CopperListState::Initialized);
3022 culist.msgs.0 = 41;
3023 culist.change_state(CopperListState::Processing);
3024 }
3025
3026 copperlists.end_of_processing(0).unwrap();
3027 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3028
3029 let reused = copperlists.create().unwrap();
3030 assert_eq!(reused.id, 1);
3031 assert_eq!(reused.get_state(), CopperListState::Initialized);
3032 assert_eq!(reused.msgs.0, 41);
3033 }
3034
3035 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
3036 #[test]
3037 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
3038 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
3039 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3040
3041 let culist = copperlists.create().unwrap();
3042 assert_eq!(culist.id, 0);
3043 assert_eq!(culist.get_state(), CopperListState::Initialized);
3044
3045 let _ = copperlists.end_of_processing(0);
3046 }
3047
3048 #[test]
3049 fn test_runtime_task_input_order() {
3050 let mut config = CuConfig::default();
3051 let graph = config.get_graph_mut(None).unwrap();
3052 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
3053 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
3054 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
3055
3056 assert_eq!(src1_id, 0);
3057 assert_eq!(src2_id, 1);
3058
3059 let src1_type = "src1_type";
3061 let src2_type = "src2_type";
3062 graph.connect(src2_id, sink_id, src2_type).unwrap();
3063 graph.connect(src1_id, sink_id, src1_type).unwrap();
3064
3065 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
3066 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
3067 assert_eq!(src1_edge_id, 1);
3070 assert_eq!(src2_edge_id, 0);
3071
3072 let runtime = compute_runtime_plan(graph).unwrap();
3073 let sink_step = runtime
3074 .steps
3075 .iter()
3076 .find_map(|step| match step {
3077 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
3078 _ => None,
3079 })
3080 .unwrap();
3081
3082 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
3085 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
3086 }
3087
3088 #[test]
3089 fn test_runtime_output_ports_unique_ordered() {
3090 let mut config = CuConfig::default();
3091 let graph = config.get_graph_mut(None).unwrap();
3092 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3093 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
3094 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
3095 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
3096 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
3097
3098 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
3099 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
3100 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
3101 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
3102
3103 let runtime = compute_runtime_plan(graph).unwrap();
3104 let src_step = runtime
3105 .steps
3106 .iter()
3107 .find_map(|step| match step {
3108 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3109 _ => None,
3110 })
3111 .unwrap();
3112
3113 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3114 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
3115
3116 let dst_a_step = runtime
3117 .steps
3118 .iter()
3119 .find_map(|step| match step {
3120 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
3121 _ => None,
3122 })
3123 .unwrap();
3124 let dst_b_step = runtime
3125 .steps
3126 .iter()
3127 .find_map(|step| match step {
3128 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
3129 _ => None,
3130 })
3131 .unwrap();
3132 let dst_a2_step = runtime
3133 .steps
3134 .iter()
3135 .find_map(|step| match step {
3136 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
3137 _ => None,
3138 })
3139 .unwrap();
3140 let dst_c_step = runtime
3141 .steps
3142 .iter()
3143 .find_map(|step| match step {
3144 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
3145 _ => None,
3146 })
3147 .unwrap();
3148
3149 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
3150 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
3151 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
3152 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
3153 }
3154
3155 #[test]
3156 fn test_runtime_output_ports_fanout_single() {
3157 let mut config = CuConfig::default();
3158 let graph = config.get_graph_mut(None).unwrap();
3159 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3160 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
3161 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
3162
3163 graph.connect(src_id, dst_a_id, "i32").unwrap();
3164 graph.connect(src_id, dst_b_id, "i32").unwrap();
3165
3166 let runtime = compute_runtime_plan(graph).unwrap();
3167 let src_step = runtime
3168 .steps
3169 .iter()
3170 .find_map(|step| match step {
3171 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3172 _ => None,
3173 })
3174 .unwrap();
3175
3176 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3177 assert_eq!(output_pack.msg_types, vec!["i32"]);
3178 }
3179
3180 #[test]
3181 fn test_runtime_output_ports_include_nc_outputs() {
3182 let mut config = CuConfig::default();
3183 let graph = config.get_graph_mut(None).unwrap();
3184 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3185 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
3186 graph.connect(src_id, dst_id, "msg::A").unwrap();
3187 graph
3188 .get_node_mut(src_id)
3189 .expect("missing source node")
3190 .add_nc_output("msg::B", usize::MAX);
3191
3192 let runtime = compute_runtime_plan(graph).unwrap();
3193 let src_step = runtime
3194 .steps
3195 .iter()
3196 .find_map(|step| match step {
3197 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3198 _ => None,
3199 })
3200 .unwrap();
3201 let dst_step = runtime
3202 .steps
3203 .iter()
3204 .find_map(|step| match step {
3205 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3206 _ => None,
3207 })
3208 .unwrap();
3209
3210 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3211 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3212 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
3213 }
3214
3215 #[test]
3216 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
3217 let txt = r#"(
3218 tasks: [
3219 (id: "src", type: "a"),
3220 (id: "regular", type: "b"),
3221 ],
3222 cnx: [
3223 (src: "src", dst: "regular", msg: "msg::A"),
3224 (src: "regular", dst: "__nc__", msg: "msg::B"),
3225 ]
3226 )"#;
3227 let config = CuConfig::deserialize_ron(txt).unwrap();
3228 let graph = config.get_graph(None).unwrap();
3229 let regular_id = graph.get_node_id_by_name("regular").unwrap();
3230
3231 let runtime = compute_runtime_plan(graph).unwrap();
3232 let regular_step = runtime
3233 .steps
3234 .iter()
3235 .find_map(|step| match step {
3236 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
3237 _ => None,
3238 })
3239 .unwrap();
3240
3241 assert_eq!(regular_step.task_type, CuTaskType::Regular);
3242 assert_eq!(
3243 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
3244 vec!["msg::B"]
3245 );
3246 }
3247
3248 #[test]
3249 fn test_runtime_output_ports_respect_connection_order_with_nc() {
3250 let txt = r#"(
3251 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3252 cnx: [
3253 (src: "src", dst: "__nc__", msg: "msg::A"),
3254 (src: "src", dst: "sink", msg: "msg::B"),
3255 ]
3256 )"#;
3257 let config = CuConfig::deserialize_ron(txt).unwrap();
3258 let graph = config.get_graph(None).unwrap();
3259 let src_id = graph.get_node_id_by_name("src").unwrap();
3260 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3261
3262 let runtime = compute_runtime_plan(graph).unwrap();
3263 let src_step = runtime
3264 .steps
3265 .iter()
3266 .find_map(|step| match step {
3267 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3268 _ => None,
3269 })
3270 .unwrap();
3271 let dst_step = runtime
3272 .steps
3273 .iter()
3274 .find_map(|step| match step {
3275 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3276 _ => None,
3277 })
3278 .unwrap();
3279
3280 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3281 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3282 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3283 }
3284
3285 #[cfg(feature = "std")]
3286 #[test]
3287 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
3288 let txt = r#"(
3289 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3290 cnx: [
3291 (src: "src", dst: "__nc__", msg: "msg::A"),
3292 (src: "src", dst: "sink", msg: "msg::B"),
3293 ]
3294 )"#;
3295 let tmp = tempfile::NamedTempFile::new().unwrap();
3296 std::fs::write(tmp.path(), txt).unwrap();
3297 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
3298 let graph = config.get_graph(None).unwrap();
3299 let src_id = graph.get_node_id_by_name("src").unwrap();
3300 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3301
3302 let runtime = compute_runtime_plan(graph).unwrap();
3303 let src_step = runtime
3304 .steps
3305 .iter()
3306 .find_map(|step| match step {
3307 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3308 _ => None,
3309 })
3310 .unwrap();
3311 let dst_step = runtime
3312 .steps
3313 .iter()
3314 .find_map(|step| match step {
3315 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3316 _ => None,
3317 })
3318 .unwrap();
3319
3320 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3321 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3322 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3323 }
3324
3325 #[test]
3326 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
3327 let txt = r#"(
3328 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3329 cnx: [
3330 (src: "src", dst: "__nc__", msg: "i32"),
3331 (src: "src", dst: "sink", msg: "bool"),
3332 ]
3333 )"#;
3334 let config = CuConfig::deserialize_ron(txt).unwrap();
3335 let graph = config.get_graph(None).unwrap();
3336 let src_id = graph.get_node_id_by_name("src").unwrap();
3337 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3338
3339 let runtime = compute_runtime_plan(graph).unwrap();
3340 let src_step = runtime
3341 .steps
3342 .iter()
3343 .find_map(|step| match step {
3344 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3345 _ => None,
3346 })
3347 .unwrap();
3348 let dst_step = runtime
3349 .steps
3350 .iter()
3351 .find_map(|step| match step {
3352 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3353 _ => None,
3354 })
3355 .unwrap();
3356
3357 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3358 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
3359 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3360 }
3361
3362 #[test]
3363 fn test_runtime_plan_diamond_case1() {
3364 let mut config = CuConfig::default();
3366 let graph = config.get_graph_mut(None).unwrap();
3367 let cam0_id = graph
3368 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
3369 .unwrap();
3370 let inf0_id = graph
3371 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
3372 .unwrap();
3373 let broadcast_id = graph
3374 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
3375 .unwrap();
3376
3377 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
3379 graph.connect(cam0_id, inf0_id, "i32").unwrap();
3380 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
3381
3382 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
3383 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
3384
3385 assert_eq!(edge_cam0_to_inf0, 0);
3386 assert_eq!(edge_cam0_to_broadcast, 1);
3387
3388 let runtime = compute_runtime_plan(graph).unwrap();
3389 let broadcast_step = runtime
3390 .steps
3391 .iter()
3392 .find_map(|step| match step {
3393 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
3394 _ => None,
3395 })
3396 .unwrap();
3397
3398 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
3399 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
3400 }
3401
3402 #[test]
3403 fn test_runtime_plan_diamond_case2() {
3404 let mut config = CuConfig::default();
3406 let graph = config.get_graph_mut(None).unwrap();
3407 let cam0_id = graph
3408 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
3409 .unwrap();
3410 let inf0_id = graph
3411 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
3412 .unwrap();
3413 let broadcast_id = graph
3414 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
3415 .unwrap();
3416
3417 graph.connect(cam0_id, inf0_id, "i32").unwrap();
3419 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
3420 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
3421
3422 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
3423 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
3424
3425 assert_eq!(edge_cam0_to_broadcast, 0);
3426 assert_eq!(edge_cam0_to_inf0, 1);
3427
3428 let runtime = compute_runtime_plan(graph).unwrap();
3429 let broadcast_step = runtime
3430 .steps
3431 .iter()
3432 .find_map(|step| match step {
3433 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
3434 _ => None,
3435 })
3436 .unwrap();
3437
3438 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
3439 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
3440 }
3441
3442 use crate::config::AnytimeConfig;
3445
3446 fn anytime_node(id: &str, max_refines: Option<u32>) -> Node {
3447 let mut node = Node::new(id, "tasks::AnytimeTask");
3448 node.set_anytime(Some(AnytimeConfig {
3449 max_refines,
3450 ..Default::default()
3451 }));
3452 node
3453 }
3454
3455 fn plan_shape(plan: &CuExecutionLoop) -> Vec<(NodeId, CuStepPhase)> {
3457 plan.steps
3458 .iter()
3459 .map(|unit| match unit {
3460 CuExecutionUnit::Step(step) => (step.node_id, step.phase),
3461 CuExecutionUnit::Loop(_) => panic!("no loops expected"),
3462 })
3463 .collect()
3464 }
3465
3466 fn manual_step(node: Node, node_id: NodeId, inputs: &[u32], output: u32) -> CuExecutionUnit {
3469 CuExecutionUnit::Step(Box::new(CuExecutionStep {
3470 node_id,
3471 node,
3472 task_type: CuTaskType::Regular,
3473 phase: CuStepPhase::default(),
3474 input_msg_indices_types: inputs
3475 .iter()
3476 .map(|&culist_index| CuInputMsg {
3477 culist_index,
3478 msg_type: "msg::A".to_string(),
3479 src_port: 0,
3480 edge_id: 0,
3481 connection_order: 0,
3482 })
3483 .collect(),
3484 output_msg_pack: Some(CuOutputPack {
3485 culist_index: output,
3486 msg_types: vec!["msg::A".to_string()],
3487 }),
3488 }))
3489 }
3490
3491 #[test]
3492 fn test_anytime_expansion_contiguous_without_gap() {
3493 let mut config = CuConfig::default();
3496 let graph = config.get_graph_mut(None).unwrap();
3497 let src_id = graph.add_node(Node::new("src", "tasks::Src")).unwrap();
3498 let any_id = graph.add_node(anytime_node("any", Some(3))).unwrap();
3499 let sink_id = graph.add_node(Node::new("sink", "tasks::Sink")).unwrap();
3500 graph.connect(src_id, any_id, "msg::A").unwrap();
3501 graph.connect(any_id, sink_id, "msg::B").unwrap();
3502
3503 let mut plan = compute_runtime_plan(graph).unwrap();
3504 expand_anytime_steps(&mut plan).unwrap();
3505
3506 assert_eq!(
3507 plan_shape(&plan),
3508 vec![
3509 (src_id, CuStepPhase::Whole),
3510 (any_id, CuStepPhase::AnytimeBase),
3511 (any_id, CuStepPhase::AnytimeRefine),
3512 (any_id, CuStepPhase::AnytimeRefine),
3513 (any_id, CuStepPhase::AnytimeRefine),
3514 (sink_id, CuStepPhase::Whole),
3515 ]
3516 );
3517
3518 let (base_pack, refine_steps): (Option<CuOutputPack>, Vec<&CuExecutionStep>) = {
3520 let mut base_pack = None;
3521 let mut refines = Vec::new();
3522 for unit in &plan.steps {
3523 if let CuExecutionUnit::Step(step) = unit {
3524 match step.phase {
3525 CuStepPhase::AnytimeBase => base_pack = step.output_msg_pack.clone(),
3526 CuStepPhase::AnytimeRefine => refines.push(step.as_ref()),
3527 CuStepPhase::Whole => {}
3528 }
3529 }
3530 }
3531 (base_pack, refines)
3532 };
3533 let base_pack = base_pack.unwrap();
3534 for refine in refine_steps {
3535 assert!(refine.input_msg_indices_types.is_empty());
3536 let pack = refine.output_msg_pack.as_ref().unwrap();
3537 assert_eq!(pack.culist_index, base_pack.culist_index);
3538 }
3539 }
3540
3541 #[test]
3542 fn test_anytime_expansion_interleaves_with_gap_steps() {
3543 let mut plan = CuExecutionLoop {
3546 steps: vec![
3547 manual_step(anytime_node("any", Some(4)), 0, &[], 0),
3548 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
3549 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
3550 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
3551 ],
3552 loop_count: None,
3553 };
3554 expand_anytime_steps(&mut plan).unwrap();
3555 assert_eq!(
3556 plan_shape(&plan),
3557 vec![
3558 (0, CuStepPhase::AnytimeBase),
3559 (0, CuStepPhase::AnytimeRefine),
3560 (1, CuStepPhase::Whole),
3561 (0, CuStepPhase::AnytimeRefine),
3562 (2, CuStepPhase::Whole),
3563 (0, CuStepPhase::AnytimeRefine),
3564 (0, CuStepPhase::AnytimeRefine),
3565 (3, CuStepPhase::Whole),
3566 ]
3567 );
3568 }
3569
3570 #[test]
3571 fn test_anytime_expansion_fewer_refines_than_gaps() {
3572 let mut plan = CuExecutionLoop {
3574 steps: vec![
3575 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
3576 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
3577 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
3578 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
3579 ],
3580 loop_count: None,
3581 };
3582 expand_anytime_steps(&mut plan).unwrap();
3583 assert_eq!(
3584 plan_shape(&plan),
3585 vec![
3586 (0, CuStepPhase::AnytimeBase),
3587 (0, CuStepPhase::AnytimeRefine),
3588 (1, CuStepPhase::Whole),
3589 (0, CuStepPhase::AnytimeRefine),
3590 (2, CuStepPhase::Whole),
3591 (3, CuStepPhase::Whole),
3592 ]
3593 );
3594 }
3595
3596 #[test]
3597 fn test_anytime_expansion_without_consumer() {
3598 let mut plan = CuExecutionLoop {
3600 steps: vec![
3601 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
3602 manual_step(Node::new("other", "t"), 1, &[], 1),
3603 ],
3604 loop_count: None,
3605 };
3606 expand_anytime_steps(&mut plan).unwrap();
3607 assert_eq!(
3608 plan_shape(&plan),
3609 vec![
3610 (0, CuStepPhase::AnytimeBase),
3611 (0, CuStepPhase::AnytimeRefine),
3612 (0, CuStepPhase::AnytimeRefine),
3613 (1, CuStepPhase::Whole),
3614 ]
3615 );
3616 }
3617
3618 #[test]
3619 fn test_anytime_expansion_two_nodes_interleave() {
3620 let mut plan = CuExecutionLoop {
3623 steps: vec![
3624 manual_step(anytime_node("any_a", Some(2)), 0, &[], 0),
3625 manual_step(anytime_node("any_b", Some(2)), 1, &[], 1),
3626 manual_step(Node::new("consumer", "t"), 2, &[0, 1], 2),
3627 ],
3628 loop_count: None,
3629 };
3630 expand_anytime_steps(&mut plan).unwrap();
3631 assert_eq!(
3634 plan_shape(&plan),
3635 vec![
3636 (0, CuStepPhase::AnytimeBase),
3637 (0, CuStepPhase::AnytimeRefine),
3638 (1, CuStepPhase::AnytimeBase),
3639 (1, CuStepPhase::AnytimeRefine),
3640 (0, CuStepPhase::AnytimeRefine),
3641 (1, CuStepPhase::AnytimeRefine),
3642 (2, CuStepPhase::Whole),
3643 ]
3644 );
3645 }
3646
3647 #[test]
3648 fn test_anytime_expansion_requires_max_refines() {
3649 let mut plan = CuExecutionLoop {
3650 steps: vec![
3651 manual_step(anytime_node("any", None), 0, &[], 0),
3652 manual_step(Node::new("consumer", "t"), 1, &[0], 1),
3653 ],
3654 loop_count: None,
3655 };
3656 let err = expand_anytime_steps(&mut plan).unwrap_err();
3657 assert!(err.to_string().contains("needs anytime.max_refines"));
3658 }
3659}