Skip to main content

cu29_runtime/
curuntime.rs

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