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