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