1use crate::app::Subsystem;
6use crate::config::{ComponentConfig, DEFAULT_KEYFRAME_INTERVAL, Node, TaskKind};
7use crate::config::{
8 CuConfig, CuGraph, MAX_RATE_TARGET_HZ, NodeId, RuntimeConfig, resolve_task_kind_for_id,
9};
10use crate::copperlist::{CopperList, CopperListState, CuListZeroedInit, CuListsManager};
11use crate::cutask::{BincodeAdapter, Freezable};
12#[cfg(feature = "std")]
13use crate::monitoring::ExecutionProbeHandle;
14#[cfg(feature = "std")]
15use crate::monitoring::MonitorExecutionProbe;
16use crate::monitoring::{
17 ComponentId, CopperListInfo, CuMonitor, CuMonitoringMetadata, CuMonitoringRuntime,
18 ExecutionMarker, MonitorComponentMetadata, RuntimeExecutionProbe, build_monitor_topology,
19 take_last_completed_handle_bytes,
20};
21#[cfg(all(feature = "std", feature = "parallel-rt"))]
22use crate::parallel_rt::{ParallelRt, ParallelRtMetadata};
23use crate::planner::{CuPlanner, Linearity, check_order, plan_from_order};
24use crate::resource::ResourceManager;
25#[cfg(feature = "std")]
26use alloc::sync::Arc;
27use compact_str::CompactString;
28use cu29_clock::{ClockProvider, CuDuration, CuTime, RobotClock};
29use cu29_traits::CuResult;
30use cu29_traits::WriteStream;
31use cu29_traits::{CopperListTuple, CuError};
32#[cfg(feature = "std")]
33use rayon::ThreadPool;
34
35#[cfg(target_os = "none")]
36#[allow(unused_imports)]
37use cu29_log::{ANONYMOUS, CuLogEntry, CuLogLevel};
38#[cfg(target_os = "none")]
39#[allow(unused_imports)]
40use cu29_log_derive::info;
41#[cfg(target_os = "none")]
42#[allow(unused_imports)]
43use cu29_log_runtime::log;
44#[cfg(all(target_os = "none", debug_assertions))]
45#[allow(unused_imports)]
46use cu29_log_runtime::log_debug_mode;
47#[cfg(target_os = "none")]
48#[allow(unused_imports)]
49use cu29_value::to_value;
50
51use alloc::boxed::Box;
52use alloc::format;
53use alloc::string::{String, ToString};
54use alloc::vec::Vec;
55use bincode::de::read::Reader;
56use bincode::de::{Decoder, DecoderImpl};
57use bincode::enc::EncoderImpl;
58use bincode::enc::write::{SizeWriter, Writer};
59use bincode::error::{DecodeError, EncodeError};
60use bincode::{Decode, Encode};
61use core::fmt::Result as FmtResult;
62use core::fmt::{Debug, Formatter};
63use core::marker::PhantomData;
64
65#[cfg(all(feature = "std", feature = "async-cl-io"))]
66use rtrb::{Consumer, PopError, Producer, PushError, RingBuffer};
67#[cfg(all(feature = "std", feature = "async-cl-io"))]
68use std::sync::atomic::{AtomicBool, Ordering};
69#[cfg(all(feature = "std", feature = "async-cl-io"))]
70use std::thread::{JoinHandle, Thread};
71
72#[cfg(feature = "std")]
73#[doc(hidden)]
74pub type TasksInstantiator<CT> = for<'c> fn(
75 Vec<Option<&'c ComponentConfig>>,
76 &mut ResourceManager,
77 &[Option<Arc<ThreadPool>>],
78) -> CuResult<CT>;
79#[cfg(not(feature = "std"))]
80#[doc(hidden)]
81pub type TasksInstantiator<CT> =
82 for<'c> fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>;
83#[doc(hidden)]
84pub type BridgesInstantiator<CB> = fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>;
85#[cfg(feature = "std")]
92#[doc(hidden)]
93pub type ThreadPoolsInstantiator = fn(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>;
94#[doc(hidden)]
95pub type MonitorInstantiator<M> = fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M;
96
97#[doc(hidden)]
98pub struct CuRuntimeParts<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI> {
99 pub tasks_instanciator: TI,
100 pub monitored_components: &'static [MonitorComponentMetadata],
101 pub culist_component_mapping: &'static [ComponentId],
102 #[cfg(all(feature = "std", feature = "parallel-rt"))]
103 pub parallel_rt_metadata: &'static ParallelRtMetadata,
104 pub monitor_instanciator: MI,
105 pub bridges_instanciator: BI,
106 _payload: PhantomData<(CT, CB, P, M, [(); NBCL])>,
107}
108
109impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI>
110 CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>
111{
112 pub const fn new(
113 tasks_instanciator: TI,
114 monitored_components: &'static [MonitorComponentMetadata],
115 culist_component_mapping: &'static [ComponentId],
116 #[cfg(all(feature = "std", feature = "parallel-rt"))]
117 parallel_rt_metadata: &'static ParallelRtMetadata,
118 monitor_instanciator: MI,
119 bridges_instanciator: BI,
120 ) -> Self {
121 Self {
122 tasks_instanciator,
123 monitored_components,
124 culist_component_mapping,
125 #[cfg(all(feature = "std", feature = "parallel-rt"))]
126 parallel_rt_metadata,
127 monitor_instanciator,
128 bridges_instanciator,
129 _payload: PhantomData,
130 }
131 }
132}
133
134#[doc(hidden)]
135pub struct CuRuntimeBuilder<
136 'cfg,
137 CT,
138 CB,
139 P: CopperListTuple,
140 M: CuMonitor,
141 const NBCL: usize,
142 TI,
143 BI,
144 MI,
145 CLS,
146 KFS,
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 copperlist_sink: CLS,
158 keyframe_sink: KFS,
159 output_requirements: OutputRequirements,
160}
161
162impl<'cfg, CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI, CLS, KFS>
163 CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
164{
165 pub fn new(
166 clock: RobotClock,
167 config: &'cfg CuConfig,
168 mission: &'cfg str,
169 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
170 copperlist_sink: CLS,
171 keyframe_sink: KFS,
172 output_requirements: OutputRequirements,
173 ) -> Self {
174 Self {
175 clock,
176 config,
177 mission,
178 subsystem: Subsystem::new(None, 0),
179 instance_id: 0,
180 resources: None,
181 #[cfg(feature = "std")]
182 thread_pools: None,
183 parts,
184 copperlist_sink,
185 keyframe_sink,
186 output_requirements,
187 }
188 }
189
190 pub fn with_subsystem(mut self, subsystem: Subsystem) -> Self {
191 self.subsystem = subsystem;
192 self
193 }
194
195 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
196 self.instance_id = instance_id;
197 self
198 }
199
200 pub fn with_resources(mut self, resources: ResourceManager) -> Self {
201 self.resources = Some(resources);
202 self
203 }
204
205 pub fn try_with_resources_instantiator(
206 mut self,
207 resources_instantiator: impl FnOnce(&CuConfig) -> CuResult<ResourceManager>,
208 ) -> CuResult<Self> {
209 self.resources = Some(resources_instantiator(self.config)?);
210 Ok(self)
211 }
212
213 #[cfg(feature = "std")]
217 pub fn with_thread_pools(mut self, pools: Vec<Option<Arc<ThreadPool>>>) -> Self {
218 self.thread_pools = Some(pools);
219 self
220 }
221
222 #[cfg(feature = "std")]
223 pub fn try_with_thread_pools_instantiator(
224 mut self,
225 thread_pools_instantiator: impl FnOnce(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>,
226 ) -> CuResult<Self> {
227 self.thread_pools = Some(thread_pools_instantiator(self.config)?);
228 Ok(self)
229 }
230}
231
232#[inline]
243pub fn perf_now(_clock: &RobotClock) -> CuTime {
244 #[cfg(all(feature = "std", feature = "sysclock-perf"))]
245 {
246 static PERF_CLOCK: std::sync::OnceLock<RobotClock> = std::sync::OnceLock::new();
247 return PERF_CLOCK.get_or_init(RobotClock::new).now();
248 }
249
250 #[allow(unreachable_code)]
251 _clock.now()
252}
253
254#[cfg(all(feature = "std", feature = "high-precision-limiter"))]
255const HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS: u64 = 200_000;
256
257#[inline]
259pub fn rate_target_period(rate_target_hz: u64) -> CuResult<CuDuration> {
260 if rate_target_hz == 0 {
261 return Err(CuError::from(
262 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
263 ));
264 }
265
266 if rate_target_hz > MAX_RATE_TARGET_HZ {
267 return Err(CuError::from(format!(
268 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
269 )));
270 }
271
272 Ok(CuDuration::from(MAX_RATE_TARGET_HZ / rate_target_hz))
273}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
282pub struct LoopRateLimiter {
283 period: CuDuration,
284 next_deadline: CuTime,
285}
286
287impl LoopRateLimiter {
288 #[inline]
289 pub fn from_rate_target_hz(rate_target_hz: u64, clock: &RobotClock) -> CuResult<Self> {
290 let period = rate_target_period(rate_target_hz)?;
291 Ok(Self {
292 period,
293 next_deadline: clock.now() + period,
294 })
295 }
296
297 #[inline]
298 pub fn is_ready(&self, clock: &RobotClock) -> bool {
299 self.remaining(clock).is_none()
300 }
301
302 #[inline]
303 pub fn remaining(&self, clock: &RobotClock) -> Option<CuDuration> {
304 let now = clock.now();
305 if now < self.next_deadline {
306 Some(self.next_deadline - now)
307 } else {
308 None
309 }
310 }
311
312 #[inline]
313 pub fn wait_until_ready(&self, clock: &RobotClock) {
314 let deadline = self.next_deadline;
315 let Some(remaining) = self.remaining(clock) else {
316 return;
317 };
318
319 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
320 {
321 let spin_window = self.spin_window();
322 if remaining > spin_window {
323 std::thread::sleep(std::time::Duration::from(remaining - spin_window));
324 }
325 while clock.now() < deadline {
326 core::hint::spin_loop();
327 }
328 }
329
330 #[cfg(all(feature = "std", not(feature = "high-precision-limiter")))]
331 {
332 let _ = deadline;
333 std::thread::sleep(std::time::Duration::from(remaining));
334 }
335
336 #[cfg(not(feature = "std"))]
337 {
338 let _ = remaining;
339 while clock.now() < deadline {
340 core::hint::spin_loop();
341 }
342 }
343 }
344
345 #[inline]
346 pub fn mark_tick(&mut self, clock: &RobotClock) {
347 self.advance_from(clock.now());
348 }
349
350 #[inline]
351 pub fn limit(&mut self, clock: &RobotClock) {
352 self.wait_until_ready(clock);
353 self.mark_tick(clock);
354 }
355
356 #[inline]
357 fn advance_from(&mut self, now: CuTime) {
358 let steps = if now < self.next_deadline {
359 1
360 } else {
361 (now - self.next_deadline).as_nanos() / self.period.as_nanos() + 1
362 };
363 self.next_deadline += steps * self.period;
364 }
365
366 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
367 #[inline]
368 fn spin_window(&self) -> CuDuration {
369 let _ = self.period;
370 CuDuration::from(HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS)
371 }
372
373 #[cfg(test)]
374 #[inline]
375 fn next_deadline(&self) -> CuTime {
376 self.next_deadline
377 }
378}
379
380#[cfg(all(feature = "std", feature = "async-cl-io"))]
381#[doc(hidden)]
382pub trait AsyncCopperListPayload: Send {}
383
384#[cfg(all(feature = "std", feature = "async-cl-io"))]
385impl<T: Send> AsyncCopperListPayload for T {}
386
387#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
388#[doc(hidden)]
389pub trait AsyncCopperListPayload {}
390
391#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
392impl<T> AsyncCopperListPayload for T {}
393
394#[derive(Clone, Copy, Debug, PartialEq, Eq)]
401#[doc(hidden)]
402pub enum ProcessStepOutcome {
403 Continue,
404 AbortCopperList,
405}
406
407#[doc(hidden)]
409pub type ProcessStepResult = CuResult<ProcessStepOutcome>;
410
411#[cfg(feature = "remote-debug")]
412fn encode_completed_copperlist_snapshot<P: CopperListTuple>(
413 cl: &CopperList<P>,
414) -> CuResult<Vec<u8>> {
415 bincode::encode_to_vec(cl, bincode::config::standard())
416 .map_err(|e| CuError::new_with_cause("Failed to encode completed CopperList snapshot", e))
417}
418
419#[doc(hidden)]
425pub type SemanticRecordSink<T> = dyn WriteStream<T>;
426
427#[doc(hidden)]
429pub type CompletedCopperListSink<P> = SemanticRecordSink<CopperList<P>>;
430
431#[doc(hidden)]
433pub type CompletedKeyFrameSink = SemanticRecordSink<KeyFrame>;
434
435#[derive(Clone, Copy, Debug, Default)]
437#[doc(hidden)]
438pub struct NullWriteStream;
439
440impl<E: Encode> WriteStream<E> for NullWriteStream {
441 #[inline]
442 fn log(&mut self, _record: &E) -> CuResult<()> {
443 Ok(())
444 }
445}
446
447#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
453#[doc(hidden)]
454pub struct OutputRequirements {
455 completed_copperlists: bool,
456 keyframes: bool,
457}
458
459impl OutputRequirements {
460 #[inline]
461 pub const fn new(completed_copperlists: bool, keyframes: bool) -> Self {
462 Self {
463 completed_copperlists,
464 keyframes,
465 }
466 }
467
468 #[inline]
469 pub const fn completed_copperlists(self) -> bool {
470 self.completed_copperlists
471 }
472
473 #[inline]
474 pub const fn keyframes(self) -> bool {
475 self.keyframes
476 }
477
478 #[inline]
480 pub const fn union(self, other: Self) -> Self {
481 Self::new(
482 self.completed_copperlists || other.completed_copperlists,
483 self.keyframes || other.keyframes,
484 )
485 }
486}
487
488#[doc(hidden)]
490pub struct SyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
491 inner: CuListsManager<P, NBCL>,
492 sink: Option<Box<CompletedCopperListSink<P>>>,
493 #[cfg(feature = "remote-debug")]
495 last_completed_encoded: Option<Vec<u8>>,
496 pub last_encoded_bytes: u64,
498 pub last_handle_bytes: u64,
500}
501
502impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
503 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
504 where
505 P: CuListZeroedInit,
506 {
507 Ok(Self {
508 inner: CuListsManager::new(),
509 sink,
510 #[cfg(feature = "remote-debug")]
511 last_completed_encoded: None,
512 last_encoded_bytes: 0,
513 last_handle_bytes: 0,
514 })
515 }
516
517 pub fn next_cl_id(&self) -> u64 {
518 self.inner.next_cl_id()
519 }
520
521 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
524 if !self.inner.is_empty() {
525 return Err(CuError::from(
526 "Cannot reposition replay with active CopperLists",
527 ));
528 }
529 self.inner.set_next_replay_id(next_id);
530 Ok(())
531 }
532
533 pub fn last_cl_id(&self) -> u64 {
534 self.inner.last_cl_id()
535 }
536
537 pub fn peek(&self) -> Option<&CopperList<P>> {
538 self.inner.peek()
539 }
540
541 #[cfg(feature = "remote-debug")]
542 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
543 self.last_completed_encoded.as_deref()
544 }
545
546 #[cfg(not(feature = "remote-debug"))]
547 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
548 None
549 }
550
551 #[cfg(feature = "remote-debug")]
552 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
553 self.last_completed_encoded = snapshot;
554 }
555
556 #[cfg(not(feature = "remote-debug"))]
557 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
558
559 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
560 where
561 P: CuListZeroedInit,
562 {
563 self.inner
564 .create()
565 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))
566 }
567
568 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
569 #[cfg(debug_assertions)]
570 self.debug_assert_end_of_processing_target(culistid);
571
572 let mut is_top = true;
573 let mut nb_done = 0;
574 self.last_encoded_bytes = 0;
575 self.last_handle_bytes = 0;
576 #[cfg(feature = "remote-debug")]
577 let last_completed_encoded = &mut self.last_completed_encoded;
578 for cl in self.inner.iter_mut() {
579 if cl.id == culistid && cl.get_state() == CopperListState::Processing {
580 cl.change_state(CopperListState::DoneProcessing);
581 #[cfg(feature = "remote-debug")]
582 {
583 *last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
584 }
585 }
586 if is_top && cl.get_state() == CopperListState::DoneProcessing {
587 if let Some(sink) = &mut self.sink {
588 cl.change_state(CopperListState::BeingSerialized);
589 sink.log(cl)?;
590 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
591 self.last_handle_bytes = take_last_completed_handle_bytes();
592 }
593 cl.change_state(CopperListState::Free);
594 nb_done += 1;
595 } else {
596 is_top = false;
597 }
598 }
599 for _ in 0..nb_done {
600 let _ = self.inner.pop();
601 }
602 Ok(())
603 }
604
605 pub fn finish_pending(&mut self) -> CuResult<()> {
606 Ok(())
607 }
608
609 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
610 Ok(NBCL - self.inner.len())
611 }
612
613 #[inline]
614 pub const fn dropped_copperlists_total(&self) -> u64 {
615 0
616 }
617
618 #[cfg(feature = "std")]
619 pub fn end_of_processing_boxed(
620 &mut self,
621 mut culist: Box<CopperList<P>>,
622 ) -> CuResult<OwnedCopperListSubmission<P>> {
623 #[cfg(debug_assertions)]
624 debug_assert_processing_completion_state(culist.as_ref(), "sync boxed end_of_processing");
625
626 culist.change_state(CopperListState::DoneProcessing);
627 self.last_encoded_bytes = 0;
628 self.last_handle_bytes = 0;
629 if let Some(sink) = &mut self.sink {
630 culist.change_state(CopperListState::BeingSerialized);
631 sink.log(&culist)?;
632 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
633 self.last_handle_bytes = take_last_completed_handle_bytes();
634 }
635 culist.change_state(CopperListState::Free);
636 Ok(OwnedCopperListSubmission::Recycled(culist))
637 }
638
639 #[cfg(feature = "std")]
640 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
641 Ok(None)
642 }
643
644 #[cfg(feature = "std")]
645 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
646 Err(CuError::from(
647 "Synchronous CopperList I/O cannot block waiting for boxed completions",
648 ))
649 }
650
651 #[cfg(feature = "std")]
652 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
653 Ok(Vec::new())
654 }
655
656 #[cfg(debug_assertions)]
657 fn debug_assert_end_of_processing_target(&self, culistid: u64) {
658 let mut matches = 0usize;
659 let mut state = None;
660 for cl in self.inner.iter() {
661 if cl.id == culistid {
662 matches += 1;
663 state = Some(cl.get_state());
664 }
665 }
666
667 assert_eq!(
668 matches, 1,
669 "sync end_of_processing expected exactly one active CopperList #{culistid}, found {matches}"
670 );
671 assert_eq!(
672 state,
673 Some(CopperListState::Processing),
674 "sync end_of_processing expected CopperList #{culistid} to be Processing, found {:?}",
675 state
676 );
677 }
678}
679
680#[cfg(feature = "std")]
682#[doc(hidden)]
683pub enum OwnedCopperListSubmission<P: CopperListTuple> {
684 Recycled(Box<CopperList<P>>),
686 Pending,
688}
689
690#[cfg(all(feature = "std", feature = "async-cl-io"))]
691struct AsyncCopperListCompletion<P: CopperListTuple> {
692 culist: Box<CopperList<P>>,
693 sink_result: CuResult<(u64, u64)>,
694 #[cfg(feature = "remote-debug")]
695 completed_snapshot: CuResult<Vec<u8>>,
696}
697
698#[cfg(all(feature = "std", feature = "async-cl-io"))]
699struct AsyncOutputWorkerRunningGuard(Arc<AtomicBool>);
700
701#[cfg(all(feature = "std", feature = "async-cl-io"))]
702impl Drop for AsyncOutputWorkerRunningGuard {
703 fn drop(&mut self) {
704 self.0.store(false, Ordering::Release);
705 }
706}
707
708#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
709fn allocate_copperlist<P>() -> Box<CopperList<P>>
710where
711 P: CopperListTuple + CuListZeroedInit,
712{
713 let mut culist = Box::<CopperList<P>>::new_uninit();
714 unsafe {
716 CopperList::init_in_place(culist.as_mut_ptr());
717 culist.assume_init()
718 }
719}
720
721#[cfg(all(feature = "std", feature = "parallel-rt"))]
722pub fn allocate_boxed_copperlists<P, const NBCL: usize>() -> Vec<Box<CopperList<P>>>
723where
724 P: CopperListTuple + CuListZeroedInit,
725{
726 let mut free_pool = Vec::with_capacity(NBCL);
727 for _ in 0..NBCL {
728 free_pool.push(allocate_copperlist::<P>());
729 }
730 free_pool
731}
732
733#[cfg(all(feature = "std", feature = "async-cl-io"))]
735#[doc(hidden)]
736pub struct AsyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
737 free_pool: Vec<Box<CopperList<P>>>,
738 current: Option<Box<CopperList<P>>>,
739 #[cfg(feature = "remote-debug")]
740 last_completed_encoded: Option<Vec<u8>>,
741 pending_count: usize,
742 next_cl_id: u64,
743 pending_producer: Option<Producer<Box<CopperList<P>>>>,
744 completion_consumer: Option<Consumer<AsyncCopperListCompletion<P>>>,
745 worker_handle: Option<JoinHandle<()>>,
746 worker_thread: Option<Thread>,
747 worker_shutdown: Option<Arc<AtomicBool>>,
748 worker_running: Option<Arc<AtomicBool>>,
749 dropped_copperlists_total: u64,
750 pub last_encoded_bytes: u64,
752 pub last_handle_bytes: u64,
754}
755
756#[cfg(all(feature = "std", feature = "async-cl-io"))]
757impl<P: CopperListTuple + Default, const NBCL: usize> AsyncCopperListsManager<P, NBCL> {
758 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
759 where
760 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
761 {
762 let mut free_pool = Vec::with_capacity(NBCL);
763 for _ in 0..NBCL {
764 free_pool.push(allocate_copperlist::<P>());
765 }
766
767 if sink.is_some() && NBCL < 2 {
768 return Err(CuError::from(
769 "async CopperList output requires at least two CopperList slots",
770 ));
771 }
772
773 let (
774 pending_producer,
775 completion_consumer,
776 worker_handle,
777 worker_thread,
778 worker_shutdown,
779 worker_running,
780 ) = if let Some(mut sink) = sink {
781 let handoff_capacity = NBCL - 1;
782 let (pending_producer, mut pending_consumer) =
783 RingBuffer::<Box<CopperList<P>>>::new(handoff_capacity);
784 let (mut completion_producer, completion_consumer) =
785 RingBuffer::<AsyncCopperListCompletion<P>>::new(handoff_capacity);
786 let worker_shutdown = Arc::new(AtomicBool::new(false));
787 let worker_running = Arc::new(AtomicBool::new(true));
788 let shutdown = worker_shutdown.clone();
789 let running = worker_running.clone();
790 let worker_handle = std::thread::Builder::new()
791 .name("cu-async-cl-io".to_string())
792 .spawn(move || {
793 let _running_guard = AsyncOutputWorkerRunningGuard(running);
794 loop {
795 let mut culist = match pending_consumer.pop() {
796 Ok(culist) => culist,
797 Err(PopError::Empty) => {
798 if shutdown.load(Ordering::Acquire) {
799 break;
800 }
801 std::thread::park();
802 continue;
803 }
804 };
805 #[cfg(feature = "remote-debug")]
806 let completed_snapshot = {
807 culist.change_state(CopperListState::DoneProcessing);
810 encode_completed_copperlist_snapshot(&culist)
811 };
812 culist.change_state(CopperListState::BeingSerialized);
813 let sink_result = sink.log(&culist).map(|_| {
814 (
815 sink.last_log_bytes().unwrap_or(0) as u64,
816 take_last_completed_handle_bytes(),
817 )
818 });
819 let should_stop = sink_result.is_err();
820 #[cfg(feature = "remote-debug")]
821 let should_stop = should_stop || completed_snapshot.is_err();
822 let mut completion = AsyncCopperListCompletion {
823 culist,
824 sink_result,
825 #[cfg(feature = "remote-debug")]
826 completed_snapshot,
827 };
828 loop {
829 match completion_producer.push(completion) {
830 Ok(()) => break,
831 Err(PushError::Full(returned)) => {
832 completion = returned;
833 std::thread::yield_now();
834 }
835 }
836 }
837 if should_stop {
838 break;
839 }
840 }
841 })
842 .map_err(|e| {
843 CuError::from("Failed to spawn async CopperList serializer thread")
844 .add_cause(e.to_string().as_str())
845 })?;
846 let worker_thread = worker_handle.thread().clone();
847 (
848 Some(pending_producer),
849 Some(completion_consumer),
850 Some(worker_handle),
851 Some(worker_thread),
852 Some(worker_shutdown),
853 Some(worker_running),
854 )
855 } else {
856 (None, None, None, None, None, None)
857 };
858
859 Ok(Self {
860 free_pool,
861 current: None,
862 #[cfg(feature = "remote-debug")]
863 last_completed_encoded: None,
864 pending_count: 0,
865 next_cl_id: 0,
866 pending_producer,
867 completion_consumer,
868 worker_handle,
869 worker_thread,
870 worker_shutdown,
871 worker_running,
872 dropped_copperlists_total: 0,
873 last_encoded_bytes: 0,
874 last_handle_bytes: 0,
875 })
876 }
877
878 pub fn next_cl_id(&self) -> u64 {
879 self.next_cl_id
880 }
881
882 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
885 self.finish_pending()?;
886 self.next_cl_id = next_id;
887 Ok(())
888 }
889
890 pub fn last_cl_id(&self) -> u64 {
891 self.next_cl_id.saturating_sub(1)
892 }
893
894 pub fn peek(&self) -> Option<&CopperList<P>> {
895 self.current.as_deref()
896 }
897
898 #[cfg(feature = "remote-debug")]
899 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
900 self.last_completed_encoded.as_deref()
901 }
902
903 #[cfg(not(feature = "remote-debug"))]
904 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
905 None
906 }
907
908 #[cfg(feature = "remote-debug")]
909 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
910 self.last_completed_encoded = snapshot;
911 }
912
913 #[cfg(not(feature = "remote-debug"))]
914 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
915
916 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
917 where
918 P: CuListZeroedInit,
919 {
920 if self.current.is_some() {
921 return Err(CuError::from(
922 "Attempted to create a CopperList while another one is still active",
923 ));
924 }
925
926 self.reclaim_completed()?;
927
928 let culist = self.free_pool.pop().ok_or_else(|| {
929 CuError::from("CopperList output handoff exhausted the slot reserved for execution")
930 })?;
931 self.current = Some(culist);
932
933 let current = self
934 .current
935 .as_mut()
936 .expect("current CopperList is missing");
937 current.reset_for_runtime_use(self.next_cl_id);
938 self.next_cl_id += 1;
939 Ok(current.as_mut())
940 }
941
942 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
943 self.reclaim_completed()?;
944
945 let mut culist = self.current.take().ok_or_else(|| {
946 CuError::from("Attempted to finish processing without an active CopperList")
947 })?;
948
949 if culist.id != culistid {
950 return Err(CuError::from(format!(
951 "Attempted to finish CopperList #{culistid} while CopperList #{} is active",
952 culist.id
953 )));
954 }
955 #[cfg(debug_assertions)]
956 debug_assert_processing_completion_state(culist.as_ref(), "async end_of_processing");
957
958 culist.change_state(CopperListState::DoneProcessing);
959 self.last_encoded_bytes = 0;
960 self.last_handle_bytes = 0;
961
962 match self.try_submit(culist)? {
963 OwnedCopperListSubmission::Recycled(culist) => self.free_pool.push(culist),
964 OwnedCopperListSubmission::Pending => {}
965 }
966
967 Ok(())
968 }
969
970 pub fn finish_pending(&mut self) -> CuResult<()> {
971 if self.current.is_some() {
972 return Err(CuError::from(
973 "Cannot flush CopperList I/O while a CopperList is still active",
974 ));
975 }
976
977 while self.pending_count > 0 {
978 self.wait_for_completion()?;
979 }
980 Ok(())
981 }
982
983 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
984 self.reclaim_completed()?;
985 Ok(self.free_pool.len())
986 }
987
988 #[inline]
989 pub const fn dropped_copperlists_total(&self) -> u64 {
990 self.dropped_copperlists_total
991 }
992
993 pub fn end_of_processing_boxed(
994 &mut self,
995 mut culist: Box<CopperList<P>>,
996 ) -> CuResult<OwnedCopperListSubmission<P>> {
997 #[cfg(debug_assertions)]
998 debug_assert_processing_completion_state(culist.as_ref(), "async boxed end_of_processing");
999 culist.change_state(CopperListState::DoneProcessing);
1000 self.last_encoded_bytes = 0;
1001 self.last_handle_bytes = 0;
1002
1003 self.try_submit(culist)
1004 }
1005
1006 fn try_submit(
1007 &mut self,
1008 mut culist: Box<CopperList<P>>,
1009 ) -> CuResult<OwnedCopperListSubmission<P>> {
1010 let Some(pending_producer) = self.pending_producer.as_mut() else {
1011 culist.change_state(CopperListState::Free);
1012 return Ok(OwnedCopperListSubmission::Recycled(culist));
1013 };
1014
1015 if self.pending_count >= NBCL - 1 {
1019 return Ok(self.drop_copperlist(culist));
1020 }
1021
1022 culist.change_state(CopperListState::QueuedForSerialization);
1023 match pending_producer.push(culist) {
1024 Ok(()) => {
1025 self.pending_count += 1;
1026 if let Some(worker_thread) = self.worker_thread.as_ref() {
1027 worker_thread.unpark();
1028 }
1029 Ok(OwnedCopperListSubmission::Pending)
1030 }
1031 Err(PushError::Full(culist)) => Ok(self.drop_copperlist(culist)),
1032 }
1033 }
1034
1035 fn drop_copperlist(&mut self, mut culist: Box<CopperList<P>>) -> OwnedCopperListSubmission<P> {
1036 self.dropped_copperlists_total = self.dropped_copperlists_total.saturating_add(1);
1037 culist.change_state(CopperListState::Free);
1038 OwnedCopperListSubmission::Recycled(culist)
1039 }
1040
1041 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
1042 let pop_result = {
1043 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1044 return Ok(None);
1045 };
1046 completion_consumer.pop()
1047 };
1048 match pop_result {
1049 Ok(completion) => self.handle_completion(completion).map(Some),
1050 Err(PopError::Empty) => {
1051 if self.pending_count > 0
1052 && self
1053 .worker_running
1054 .as_ref()
1055 .is_some_and(|running| !running.load(Ordering::Acquire))
1056 {
1057 Err(CuError::from(
1058 "Async CopperList output worker stopped unexpectedly",
1059 ))
1060 } else {
1061 Ok(None)
1062 }
1063 }
1064 }
1065 }
1066
1067 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
1068 if self.completion_consumer.is_none() {
1069 return Err(CuError::from(
1070 "No async CopperList output worker is active to return a free slot",
1071 ));
1072 }
1073 loop {
1074 if let Some(culist) = self.try_reclaim_boxed()? {
1075 return Ok(culist);
1076 }
1077 std::thread::yield_now();
1078 }
1079 }
1080
1081 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
1082 let mut reclaimed = Vec::with_capacity(self.pending_count);
1083 if self.current.is_some() {
1084 return Err(CuError::from(
1085 "Cannot flush CopperList I/O while a CopperList is still active",
1086 ));
1087 }
1088 while self.pending_count > 0 {
1089 reclaimed.push(self.wait_reclaim_boxed()?);
1090 }
1091 Ok(reclaimed)
1092 }
1093
1094 fn reclaim_completed(&mut self) -> CuResult<()> {
1095 loop {
1096 let Some(culist) = self.try_reclaim_boxed()? else {
1097 break;
1098 };
1099 self.free_pool.push(culist);
1100 }
1101 Ok(())
1102 }
1103
1104 fn wait_for_completion(&mut self) -> CuResult<()> {
1105 let culist = self.wait_reclaim_boxed()?;
1106 self.free_pool.push(culist);
1107 Ok(())
1108 }
1109
1110 fn handle_completion(
1111 &mut self,
1112 mut completion: AsyncCopperListCompletion<P>,
1113 ) -> CuResult<Box<CopperList<P>>> {
1114 self.pending_count = self.pending_count.saturating_sub(1);
1115 if let Ok((encoded_bytes, handle_bytes)) = completion.sink_result.as_ref() {
1116 self.last_encoded_bytes = *encoded_bytes;
1117 self.last_handle_bytes = *handle_bytes;
1118 }
1119 completion.culist.change_state(CopperListState::Free);
1120 completion.sink_result?;
1121 #[cfg(feature = "remote-debug")]
1122 {
1123 self.last_completed_encoded = Some(completion.completed_snapshot?);
1124 }
1125 Ok(completion.culist)
1126 }
1127
1128 fn shutdown_worker(&mut self) -> CuResult<()> {
1129 self.finish_pending()?;
1130 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1131 shutdown.store(true, Ordering::Release);
1132 }
1133 if let Some(worker_thread) = self.worker_thread.as_ref() {
1134 worker_thread.unpark();
1135 }
1136 if let Some(worker_handle) = self.worker_handle.take() {
1137 worker_handle.join().map_err(|_| {
1138 CuError::from("Async CopperList output worker panicked while joining")
1139 })?;
1140 }
1141 self.pending_producer.take();
1142 self.worker_thread.take();
1143 self.worker_shutdown.take();
1144 self.worker_running.take();
1145 Ok(())
1146 }
1147}
1148
1149#[cfg(all(feature = "std", feature = "async-cl-io"))]
1150impl<P: CopperListTuple + Default, const NBCL: usize> Drop for AsyncCopperListsManager<P, NBCL> {
1151 fn drop(&mut self) {
1152 let _ = self.shutdown_worker();
1153 }
1154}
1155
1156#[cfg(all(feature = "std", debug_assertions))]
1157fn debug_assert_processing_completion_state<P: CopperListTuple>(
1158 culist: &CopperList<P>,
1159 context: &str,
1160) {
1161 assert_eq!(
1162 culist.get_state(),
1163 CopperListState::Processing,
1164 "{context} expected CopperList #{} to be Processing, found {}",
1165 culist.id,
1166 culist.get_state()
1167 );
1168}
1169
1170#[cfg(all(feature = "std", feature = "async-cl-io"))]
1171#[doc(hidden)]
1172pub type CopperListsManager<P, const NBCL: usize> = AsyncCopperListsManager<P, NBCL>;
1173
1174#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1175#[doc(hidden)]
1176pub type CopperListsManager<P, const NBCL: usize> = SyncCopperListsManager<P, NBCL>;
1177
1178#[cfg(all(feature = "std", feature = "async-cl-io"))]
1179struct AsyncKeyFrameCompletion {
1180 keyframe: Box<KeyFrame>,
1181 sink_result: CuResult<u64>,
1182}
1183
1184pub struct KeyFramesManager {
1186 inner: Option<KeyFrame>,
1188
1189 forced_timestamp: Option<CuTime>,
1191
1192 locked: bool,
1194
1195 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1197 sink: Option<Box<CompletedKeyFrameSink>>,
1198
1199 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1203 #[allow(clippy::vec_box)]
1204 spares: Vec<Box<KeyFrame>>,
1205 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1206 pending_count: usize,
1207 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1208 pending_producer: Option<Producer<Box<KeyFrame>>>,
1209 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1210 completion_consumer: Option<Consumer<AsyncKeyFrameCompletion>>,
1211 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1212 worker_handle: Option<JoinHandle<()>>,
1213 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1214 worker_thread: Option<Thread>,
1215 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1216 worker_shutdown: Option<Arc<AtomicBool>>,
1217 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1218 worker_running: Option<Arc<AtomicBool>>,
1219 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1220 capture_this_copperlist: Option<u64>,
1221 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1222 dropped_keyframes_total: u64,
1223
1224 keyframe_interval: u32,
1226
1227 pub last_encoded_bytes: u64,
1229
1230 capture_size_hint: usize,
1232}
1233
1234const MIN_KEYFRAME_CAPTURE_CAPACITY: usize = 4 * 1024;
1235#[cfg(all(feature = "std", feature = "async-cl-io"))]
1236const ASYNC_KEYFRAME_HANDOFF_CAPACITY: usize = 2;
1237
1238struct PreallocatedVecWriter<'a>(&'a mut Vec<u8>);
1240
1241impl Writer for PreallocatedVecWriter<'_> {
1242 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
1243 if bytes.len() > self.0.capacity().saturating_sub(self.0.len()) {
1244 return Err(EncodeError::UnexpectedEnd);
1245 }
1246 self.0.extend_from_slice(bytes);
1248 Ok(())
1249 }
1250}
1251
1252impl KeyFramesManager {
1253 #[doc(hidden)]
1254 pub fn new(sink: Option<Box<CompletedKeyFrameSink>>, keyframe_interval: u32) -> CuResult<Self> {
1255 if sink.is_some() && keyframe_interval == 0 {
1256 return Err(CuError::from(
1257 "Keyframe interval cannot be zero when a downstream consumer requires keyframes",
1258 ));
1259 }
1260
1261 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1262 {
1263 let enabled = sink.is_some();
1264 let (
1265 pending_producer,
1266 completion_consumer,
1267 worker_handle,
1268 worker_thread,
1269 worker_shutdown,
1270 worker_running,
1271 ) = if let Some(mut sink) = sink {
1272 let (pending_producer, mut pending_consumer) =
1273 RingBuffer::<Box<KeyFrame>>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1274 let (mut completion_producer, completion_consumer) =
1275 RingBuffer::<AsyncKeyFrameCompletion>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1276 let worker_shutdown = Arc::new(AtomicBool::new(false));
1277 let worker_running = Arc::new(AtomicBool::new(true));
1278 let shutdown = worker_shutdown.clone();
1279 let running = worker_running.clone();
1280 let worker_handle = std::thread::Builder::new()
1281 .name("cu-async-kf-io".to_string())
1282 .spawn(move || {
1283 let _running_guard = AsyncOutputWorkerRunningGuard(running);
1284 loop {
1285 let keyframe = match pending_consumer.pop() {
1286 Ok(keyframe) => keyframe,
1287 Err(PopError::Empty) => {
1288 if shutdown.load(Ordering::Acquire) {
1289 break;
1290 }
1291 std::thread::park();
1292 continue;
1293 }
1294 };
1295 let sink_result = sink
1296 .log(keyframe.as_ref())
1297 .map(|_| sink.last_log_bytes().unwrap_or(0) as u64);
1298 let should_stop = sink_result.is_err();
1299 let mut completion = AsyncKeyFrameCompletion {
1300 keyframe,
1301 sink_result,
1302 };
1303 loop {
1304 match completion_producer.push(completion) {
1305 Ok(()) => break,
1306 Err(PushError::Full(returned)) => {
1307 completion = returned;
1308 std::thread::yield_now();
1309 }
1310 }
1311 }
1312 if should_stop {
1313 break;
1314 }
1315 }
1316 })
1317 .map_err(|error| {
1318 CuError::from("Failed to spawn async keyframe output thread")
1319 .add_cause(error.to_string().as_str())
1320 })?;
1321 let worker_thread = worker_handle.thread().clone();
1322 (
1323 Some(pending_producer),
1324 Some(completion_consumer),
1325 Some(worker_handle),
1326 Some(worker_thread),
1327 Some(worker_shutdown),
1328 Some(worker_running),
1329 )
1330 } else {
1331 (None, None, None, None, None, None)
1332 };
1333
1334 let spares = if enabled {
1335 let mut spares = Vec::with_capacity(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1336 for _ in 0..ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1337 spares.push(Box::new(KeyFrame::new()));
1338 }
1339 spares
1340 } else {
1341 Vec::new()
1342 };
1343 Ok(Self {
1344 inner: enabled.then(KeyFrame::new),
1345 forced_timestamp: None,
1346 locked: false,
1347 spares,
1348 pending_count: 0,
1349 pending_producer,
1350 completion_consumer,
1351 worker_handle,
1352 worker_thread,
1353 worker_shutdown,
1354 worker_running,
1355 capture_this_copperlist: None,
1356 dropped_keyframes_total: 0,
1357 keyframe_interval,
1358 last_encoded_bytes: 0,
1359 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1360 })
1361 }
1362
1363 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1364 {
1365 let enabled = sink.is_some();
1366 Ok(Self {
1367 inner: enabled.then(KeyFrame::new),
1368 forced_timestamp: None,
1369 locked: false,
1370 sink,
1371 keyframe_interval,
1372 last_encoded_bytes: 0,
1373 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1374 })
1375 }
1376 }
1377
1378 fn is_keyframe_due(&self, culistid: u64) -> bool {
1379 self.inner.is_some() && culistid.is_multiple_of(self.keyframe_interval as u64)
1380 }
1381
1382 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1383 fn is_capturing(&self, culistid: u64) -> bool {
1384 self.capture_this_copperlist == Some(culistid)
1385 }
1386
1387 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1388 fn is_capturing(&self, culistid: u64) -> bool {
1389 self.is_keyframe_due(culistid)
1390 }
1391
1392 #[inline]
1393 pub fn captures_keyframe(&self, culistid: u64) -> bool {
1394 self.is_keyframe_due(culistid)
1395 }
1396
1397 #[doc(hidden)]
1399 pub fn begin_capture_preallocation(&mut self) {
1400 self.capture_size_hint = KEYFRAME_PAYLOAD_HEADER.len();
1401 }
1402
1403 #[doc(hidden)]
1405 pub fn include_capture_capacity(&mut self, item: &impl Freezable) -> CuResult<()> {
1406 if self.inner.is_none() {
1407 return Ok(());
1408 }
1409 let mut sizer = EncoderImpl::new(SizeWriter::default(), bincode::config::standard());
1410 BincodeAdapter(item)
1411 .encode(&mut sizer)
1412 .map_err(|_| CuError::from("Failed to size component keyframe state"))?;
1413 let payload_bytes = sizer.into_writer().bytes_written as usize;
1414 self.capture_size_hint = self
1415 .capture_size_hint
1416 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
1417 .and_then(|size| size.checked_add(payload_bytes))
1418 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1419 Ok(())
1420 }
1421
1422 #[doc(hidden)]
1424 pub fn finish_capture_preallocation(&mut self) -> CuResult<()> {
1425 if self.inner.is_none() {
1426 return Ok(());
1427 }
1428 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1429 self.reclaim_completed()?;
1430 let requested = self
1431 .capture_size_hint
1432 .max(MIN_KEYFRAME_CAPTURE_CAPACITY)
1433 .checked_next_power_of_two()
1434 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1435 reserve_keyframe_capacity(self.inner.as_mut().unwrap(), requested)?;
1436 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1437 for spare in &mut self.spares {
1438 reserve_keyframe_capacity(spare, requested)?;
1439 }
1440 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1441 if self.spares.len() != ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1442 return Err(CuError::from(
1443 "Keyframe output worker did not return every capture buffer before preallocation",
1444 ));
1445 }
1446 Ok(())
1447 }
1448
1449 #[doc(hidden)]
1452 pub fn try_reset(&mut self, culistid: u64, clock: &RobotClock) -> CuResult<()> {
1453 if self.is_keyframe_due(culistid) {
1454 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1455 {
1456 self.reclaim_completed()?;
1457 if self.spares.is_empty() {
1458 self.capture_this_copperlist = None;
1459 self.dropped_keyframes_total = self.dropped_keyframes_total.saturating_add(1);
1460 self.forced_timestamp = None;
1461 self.locked = false;
1462 return Ok(());
1463 }
1464 self.capture_this_copperlist = Some(culistid);
1465 }
1466 let inner = self.inner.as_mut().unwrap();
1468 if self.locked && inner.culistid == culistid {
1469 return Ok(());
1470 }
1471 let ts = self.forced_timestamp.take().unwrap_or_else(|| clock.now());
1472 inner.reset(culistid, ts);
1473 self.locked = false;
1474 }
1475 Ok(())
1476 }
1477
1478 pub fn reset(&mut self, culistid: u64, clock: &RobotClock) {
1483 let _ = self.try_reset(culistid, clock);
1484 }
1485
1486 #[cfg(feature = "std")]
1488 pub fn set_forced_timestamp(&mut self, ts: CuTime) {
1489 self.forced_timestamp = Some(ts);
1490 }
1491
1492 pub fn freeze_task(&mut self, culistid: u64, task: &impl Freezable) -> CuResult<usize> {
1493 if self.is_capturing(culistid) {
1494 if self.locked {
1495 return Ok(0);
1497 }
1498 let inner = self.inner.as_mut().unwrap();
1499 if inner.culistid != culistid {
1500 return Err(CuError::from(format!(
1501 "Freezing task for culistid {} but current keyframe is {}",
1502 culistid, inner.culistid
1503 )));
1504 }
1505 let encoded = inner
1506 .add_frozen_task(task)
1507 .map_err(|e| CuError::from(format!("Failed to serialize task: {e}")))?;
1508 Ok(encoded)
1509 } else {
1510 Ok(0)
1511 }
1512 }
1513
1514 pub fn freeze_any(&mut self, culistid: u64, item: &impl Freezable) -> CuResult<usize> {
1516 self.freeze_task(culistid, item)
1517 }
1518
1519 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1520 if self.is_capturing(culistid) {
1521 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1522 {
1523 let sink = self.sink.as_mut().unwrap();
1524 sink.log(self.inner.as_ref().unwrap())?;
1525 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
1526 }
1527 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1528 {
1529 self.last_encoded_bytes = 0;
1530 let mut completed = self.spares.pop().ok_or_else(|| {
1531 CuError::from("Missing spare keyframe buffer at output handoff")
1532 })?;
1533 core::mem::swap(self.inner.as_mut().unwrap(), completed.as_mut());
1534 let producer = self.pending_producer.as_mut().ok_or_else(|| {
1535 CuError::from("Missing keyframe output producer for active capture")
1536 })?;
1537 match producer.push(completed) {
1538 Ok(()) => {
1539 self.pending_count += 1;
1540 if let Some(worker_thread) = self.worker_thread.as_ref() {
1541 worker_thread.unpark();
1542 }
1543 }
1544 Err(PushError::Full(spare)) => {
1545 self.spares.push(spare);
1546 self.dropped_keyframes_total =
1547 self.dropped_keyframes_total.saturating_add(1);
1548 }
1549 }
1550 self.capture_this_copperlist = None;
1551 }
1552 self.locked = false;
1554 Ok(())
1555 } else {
1556 self.last_encoded_bytes = 0;
1558 Ok(())
1559 }
1560 }
1561
1562 #[cfg(feature = "std")]
1564 pub fn lock_keyframe(&mut self, keyframe: &KeyFrame) {
1565 if let Some(inner) = self.inner.as_mut() {
1566 *inner = keyframe.clone();
1567 self.forced_timestamp = Some(keyframe.timestamp);
1568 self.locked = true;
1569 }
1570 }
1571
1572 #[inline]
1573 #[doc(hidden)]
1574 pub const fn dropped_keyframes_total(&self) -> u64 {
1575 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1576 {
1577 self.dropped_keyframes_total
1578 }
1579 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1580 {
1581 0
1582 }
1583 }
1584
1585 #[doc(hidden)]
1586 pub fn finish_pending(&mut self) -> CuResult<()> {
1587 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1588 while self.pending_count > 0 {
1589 self.wait_for_completion()?;
1590 }
1591 Ok(())
1592 }
1593
1594 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1595 fn reclaim_completed(&mut self) -> CuResult<()> {
1596 let pop_result = {
1597 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1598 return Ok(());
1599 };
1600 completion_consumer.pop()
1601 };
1602 match pop_result {
1603 Ok(completion) => self.handle_completion(completion),
1604 Err(PopError::Empty) => {
1605 if self.pending_count > 0
1606 && self
1607 .worker_running
1608 .as_ref()
1609 .is_some_and(|running| !running.load(Ordering::Acquire))
1610 {
1611 Err(CuError::from(
1612 "Async keyframe output worker stopped unexpectedly",
1613 ))
1614 } else {
1615 Ok(())
1616 }
1617 }
1618 }
1619 }
1620
1621 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1622 fn wait_for_completion(&mut self) -> CuResult<()> {
1623 loop {
1624 let pending_before = self.pending_count;
1625 self.reclaim_completed()?;
1626 if self.pending_count < pending_before {
1627 return Ok(());
1628 }
1629 std::thread::yield_now();
1630 }
1631 }
1632
1633 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1634 fn handle_completion(&mut self, completion: AsyncKeyFrameCompletion) -> CuResult<()> {
1635 self.pending_count = self.pending_count.saturating_sub(1);
1636 if let Ok(encoded_bytes) = completion.sink_result.as_ref() {
1637 self.last_encoded_bytes = *encoded_bytes;
1638 }
1639 self.spares.push(completion.keyframe);
1640 completion.sink_result.map(|_| ())
1641 }
1642
1643 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1644 fn shutdown_worker(&mut self) -> CuResult<()> {
1645 self.finish_pending()?;
1646 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1647 shutdown.store(true, Ordering::Release);
1648 }
1649 if let Some(worker_thread) = self.worker_thread.as_ref() {
1650 worker_thread.unpark();
1651 }
1652 if let Some(worker_handle) = self.worker_handle.take() {
1653 worker_handle.join().map_err(|_| {
1654 CuError::from("Async keyframe output worker panicked while joining")
1655 })?;
1656 }
1657 self.pending_producer.take();
1658 self.worker_thread.take();
1659 self.worker_shutdown.take();
1660 self.worker_running.take();
1661 Ok(())
1662 }
1663}
1664
1665fn reserve_keyframe_capacity(keyframe: &mut KeyFrame, requested: usize) -> CuResult<()> {
1666 if keyframe.serialized_tasks.capacity() < requested {
1667 let additional = requested.saturating_sub(keyframe.serialized_tasks.len());
1668 keyframe
1669 .serialized_tasks
1670 .try_reserve_exact(additional)
1671 .map_err(|error| {
1672 CuError::from("Failed to preallocate keyframe capture buffer")
1673 .add_cause(&error.to_string())
1674 })?;
1675 }
1676 Ok(())
1677}
1678
1679#[cfg(all(feature = "std", feature = "async-cl-io"))]
1680impl Drop for KeyFramesManager {
1681 fn drop(&mut self) {
1682 let _ = self.shutdown_worker();
1683 }
1684}
1685
1686pub struct CuRuntime<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> {
1690 clock: RobotClock,
1692
1693 subsystem_code: u16,
1695
1696 #[doc(hidden)]
1698 pub instance_id: u32,
1699
1700 #[doc(hidden)]
1702 pub tasks: CT,
1703
1704 #[doc(hidden)]
1706 pub bridges: CB,
1707
1708 #[doc(hidden)]
1710 pub resources: ResourceManager,
1711
1712 #[cfg(feature = "std")]
1715 #[doc(hidden)]
1716 pub thread_pools: Vec<Option<Arc<ThreadPool>>>,
1717
1718 #[doc(hidden)]
1720 pub monitor: M,
1721
1722 #[cfg(feature = "std")]
1728 #[doc(hidden)]
1729 pub execution_probe: ExecutionProbeHandle,
1730 #[cfg(not(feature = "std"))]
1731 #[doc(hidden)]
1732 pub execution_probe: RuntimeExecutionProbe,
1733
1734 #[doc(hidden)]
1736 pub copperlists_manager: CopperListsManager<P, NBCL>,
1737
1738 #[doc(hidden)]
1740 pub keyframes_manager: KeyFramesManager,
1741
1742 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1744 #[doc(hidden)]
1745 pub parallel_rt: ParallelRt<NBCL>,
1746
1747 #[doc(hidden)]
1749 pub runtime_config: RuntimeConfig,
1750}
1751
1752impl<
1754 CT,
1755 CB,
1756 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload,
1757 M: CuMonitor,
1758 const NBCL: usize,
1759> ClockProvider for CuRuntime<CT, CB, P, M, NBCL>
1760{
1761 fn get_clock(&self) -> RobotClock {
1762 self.clock.clone()
1763 }
1764}
1765
1766impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> CuRuntime<CT, CB, P, M, NBCL> {
1767 #[inline]
1769 pub fn clock(&self) -> RobotClock {
1770 self.clock.clone()
1771 }
1772
1773 #[doc(hidden)]
1775 #[inline]
1776 pub fn clock_ref(&self) -> &RobotClock {
1777 &self.clock
1778 }
1779
1780 #[inline]
1782 pub fn subsystem_code(&self) -> u16 {
1783 self.subsystem_code
1784 }
1785
1786 #[inline]
1788 pub fn instance_id(&self) -> u32 {
1789 self.instance_id
1790 }
1791}
1792
1793#[cfg(feature = "std")]
1794impl<
1795 'cfg,
1796 CT,
1797 CB,
1798 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1799 M: CuMonitor,
1800 const NBCL: usize,
1801 TI,
1802 BI,
1803 MI,
1804 CLS,
1805 KFS,
1806> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
1807where
1808 TI: for<'c> Fn(
1809 Vec<Option<&'c ComponentConfig>>,
1810 &mut ResourceManager,
1811 &[Option<Arc<ThreadPool>>],
1812 ) -> CuResult<CT>,
1813 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1814 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1815 CLS: WriteStream<CopperList<P>> + 'static,
1816 KFS: WriteStream<KeyFrame> + 'static,
1817{
1818 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1819 let Self {
1820 clock,
1821 config,
1822 mission,
1823 subsystem,
1824 instance_id,
1825 resources,
1826 thread_pools,
1827 parts,
1828 copperlist_sink,
1829 keyframe_sink,
1830 output_requirements,
1831 } = self;
1832 let mut resources =
1833 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1834 let thread_pools = thread_pools.unwrap_or_default();
1835
1836 let graph = config.get_graph(Some(mission))?;
1837 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1838 .get_all_nodes()
1839 .iter()
1840 .map(|(_, node)| node.get_instance_config())
1841 .collect();
1842
1843 let tasks =
1844 (parts.tasks_instanciator)(all_instances_configs, &mut resources, &thread_pools)?;
1845
1846 #[cfg(feature = "std")]
1847 let execution_probe = std::sync::Arc::new(RuntimeExecutionProbe::default());
1848 #[cfg(not(feature = "std"))]
1849 let execution_probe = RuntimeExecutionProbe::default();
1850 let monitor_metadata = CuMonitoringMetadata::new(
1851 CompactString::from(mission),
1852 parts.monitored_components,
1853 parts.culist_component_mapping,
1854 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1855 build_monitor_topology(config, mission)?,
1856 None,
1857 )?
1858 .with_subsystem_id(subsystem.id())
1859 .with_instance_id(instance_id);
1860 #[cfg(feature = "std")]
1861 let monitor_runtime =
1862 CuMonitoringRuntime::new(MonitorExecutionProbe::from_shared(execution_probe.clone()));
1863 #[cfg(not(feature = "std"))]
1864 let monitor_runtime = CuMonitoringRuntime::unavailable();
1865 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1866 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1867
1868 let copperlist_sink = output_requirements
1869 .completed_copperlists()
1870 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
1871 let keyframe_sink = output_requirements
1872 .keyframes()
1873 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
1874 let keyframe_interval = config
1875 .logging
1876 .as_ref()
1877 .and_then(|logging| logging.keyframe_interval)
1878 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
1879
1880 let copperlists_manager = CopperListsManager::new(copperlist_sink)?;
1881 #[cfg(target_os = "none")]
1882 {
1883 let cl_size = core::mem::size_of::<CopperList<P>>();
1884 let total_bytes = cl_size.saturating_mul(NBCL);
1885 info!(
1886 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1887 NBCL, cl_size, total_bytes
1888 );
1889 }
1890
1891 let keyframes_manager = KeyFramesManager::new(keyframe_sink, keyframe_interval)?;
1892 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1893 let parallel_rt = ParallelRt::new(parts.parallel_rt_metadata)?;
1894
1895 let runtime_config = config.runtime.clone().unwrap_or_default();
1896 runtime_config.validate()?;
1897
1898 Ok(CuRuntime {
1899 subsystem_code: subsystem.code(),
1900 instance_id,
1901 tasks,
1902 bridges,
1903 resources,
1904 thread_pools,
1905 monitor,
1906 execution_probe,
1907 clock,
1908 copperlists_manager,
1909 keyframes_manager,
1910 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1911 parallel_rt,
1912 runtime_config,
1913 })
1914 }
1915}
1916
1917#[cfg(not(feature = "std"))]
1918impl<
1919 'cfg,
1920 CT,
1921 CB,
1922 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1923 M: CuMonitor,
1924 const NBCL: usize,
1925 TI,
1926 BI,
1927 MI,
1928 CLS,
1929 KFS,
1930> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
1931where
1932 TI: for<'c> Fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>,
1933 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1934 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1935 CLS: WriteStream<CopperList<P>> + 'static,
1936 KFS: WriteStream<KeyFrame> + 'static,
1937{
1938 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1939 let Self {
1940 clock,
1941 config,
1942 mission,
1943 subsystem,
1944 instance_id,
1945 resources,
1946 parts,
1947 copperlist_sink,
1948 keyframe_sink,
1949 output_requirements,
1950 } = self;
1951 let mut resources =
1952 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1953
1954 let graph = config.get_graph(Some(mission))?;
1955 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1956 .get_all_nodes()
1957 .iter()
1958 .map(|(_, node)| node.get_instance_config())
1959 .collect();
1960
1961 let tasks = (parts.tasks_instanciator)(all_instances_configs, &mut resources)?;
1962
1963 let execution_probe = RuntimeExecutionProbe::default();
1964 let monitor_metadata = CuMonitoringMetadata::new(
1965 CompactString::from(mission),
1966 parts.monitored_components,
1967 parts.culist_component_mapping,
1968 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1969 build_monitor_topology(config, mission)?,
1970 None,
1971 )?
1972 .with_subsystem_id(subsystem.id())
1973 .with_instance_id(instance_id);
1974 let monitor_runtime = CuMonitoringRuntime::unavailable();
1975 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1976 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1977
1978 let copperlist_sink = output_requirements
1979 .completed_copperlists()
1980 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
1981 let keyframe_sink = output_requirements
1982 .keyframes()
1983 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
1984 let keyframe_interval = config
1985 .logging
1986 .as_ref()
1987 .and_then(|logging| logging.keyframe_interval)
1988 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
1989
1990 let copperlists_manager = CopperListsManager::new(copperlist_sink)?;
1991 #[cfg(target_os = "none")]
1992 {
1993 let cl_size = core::mem::size_of::<CopperList<P>>();
1994 let total_bytes = cl_size.saturating_mul(NBCL);
1995 info!(
1996 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1997 NBCL, cl_size, total_bytes
1998 );
1999 }
2000
2001 let keyframes_manager = KeyFramesManager::new(keyframe_sink, keyframe_interval)?;
2002
2003 let runtime_config = config.runtime.clone().unwrap_or_default();
2004 runtime_config.validate()?;
2005
2006 Ok(CuRuntime {
2007 subsystem_code: subsystem.code(),
2008 instance_id,
2009 tasks,
2010 bridges,
2011 resources,
2012 monitor,
2013 execution_probe,
2014 clock,
2015 copperlists_manager,
2016 keyframes_manager,
2017 runtime_config,
2018 })
2019 }
2020}
2021
2022#[derive(Clone, Encode, Decode)]
2027pub struct KeyFrame {
2028 pub culistid: u64,
2030 pub timestamp: CuTime,
2032 pub serialized_tasks: Vec<u8>,
2034}
2035
2036impl KeyFrame {
2037 fn new() -> Self {
2038 KeyFrame {
2039 culistid: 0,
2040 timestamp: CuTime::default(),
2041 serialized_tasks: KEYFRAME_PAYLOAD_HEADER.to_vec(),
2042 }
2043 }
2044
2045 fn reset(&mut self, culistid: u64, timestamp: CuTime) {
2047 self.culistid = culistid;
2048 self.timestamp = timestamp;
2049 self.serialized_tasks.clear();
2050 self.serialized_tasks
2051 .extend_from_slice(KEYFRAME_PAYLOAD_HEADER);
2052 }
2053
2054 fn add_frozen_task(&mut self, task: &impl Freezable) -> Result<usize, EncodeError> {
2056 let cfg = bincode::config::standard();
2057 let start = self.serialized_tasks.len();
2058 let payload_offset =
2059 start
2060 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
2061 .ok_or(EncodeError::Other(
2062 "keyframe component frame offset overflow",
2063 ))?;
2064 if payload_offset > self.serialized_tasks.capacity() {
2065 return Err(EncodeError::UnexpectedEnd);
2066 }
2067
2068 self.serialized_tasks.resize(payload_offset, 0);
2069 let length_offset = start;
2070 self.serialized_tasks[length_offset..payload_offset].fill(0);
2071
2072 let mut encoder =
2073 EncoderImpl::<_, _>::new(PreallocatedVecWriter(&mut self.serialized_tasks), cfg);
2074 if let Err(error) = BincodeAdapter(task).encode(&mut encoder) {
2075 self.serialized_tasks.truncate(start);
2076 return Err(error);
2077 }
2078 let payload_len = encoder.into_writer().0.len() - payload_offset;
2079 let payload_len = u32::try_from(payload_len).map_err(|_| {
2080 self.serialized_tasks.truncate(start);
2081 EncodeError::OtherString(
2082 "keyframe component snapshot exceeds the u32 frame limit".to_string(),
2083 )
2084 })?;
2085 self.serialized_tasks
2086 .truncate(payload_offset + payload_len as usize);
2087 self.serialized_tasks[length_offset..payload_offset]
2088 .copy_from_slice(&payload_len.to_le_bytes());
2089 Ok(self.serialized_tasks.len() - start)
2090 }
2091}
2092
2093const KEYFRAME_PAYLOAD_MAGIC: &[u8; 4] = b"CUKF";
2094const KEYFRAME_PAYLOAD_VERSION: u8 = 1;
2095const KEYFRAME_PAYLOAD_HEADER: &[u8; 5] = b"CUKF\x01";
2096const KEYFRAME_FRAME_HEADER_LEN: usize = 4;
2097
2098#[doc(hidden)]
2100pub struct KeyFramePayloadReader<'a> {
2101 remaining: &'a [u8],
2102}
2103
2104impl<'a> KeyFramePayloadReader<'a> {
2105 pub fn new(keyframe: &'a KeyFrame) -> CuResult<Self> {
2107 let payload = keyframe.serialized_tasks.as_slice();
2108 if payload.len() < KEYFRAME_PAYLOAD_HEADER.len()
2109 || payload[..KEYFRAME_PAYLOAD_MAGIC.len()] != *KEYFRAME_PAYLOAD_MAGIC
2110 {
2111 return Err(CuError::from(
2112 "Unsupported legacy keyframe payload: expected framed format version 1",
2113 ));
2114 }
2115 let version = payload[KEYFRAME_PAYLOAD_MAGIC.len()];
2116 if version != KEYFRAME_PAYLOAD_VERSION {
2117 return Err(CuError::from(format!(
2118 "Unsupported keyframe payload version {version}; expected {KEYFRAME_PAYLOAD_VERSION}"
2119 )));
2120 }
2121 Ok(Self {
2122 remaining: &payload[KEYFRAME_PAYLOAD_HEADER.len()..],
2123 })
2124 }
2125
2126 pub fn next_frame(&mut self) -> CuResult<&'a [u8]> {
2128 if self.remaining.len() < KEYFRAME_FRAME_HEADER_LEN {
2129 return Err(CuError::from("Keyframe ended before next component frame"));
2130 }
2131 let payload_len = u32::from_le_bytes(
2132 self.remaining[..KEYFRAME_FRAME_HEADER_LEN]
2133 .try_into()
2134 .map_err(|_| CuError::from("Invalid keyframe component frame length"))?,
2135 ) as usize;
2136 let frame_end = KEYFRAME_FRAME_HEADER_LEN
2137 .checked_add(payload_len)
2138 .ok_or_else(|| CuError::from("Keyframe component frame length overflow"))?;
2139 if frame_end > self.remaining.len() {
2140 return Err(CuError::from("Keyframe component frame is truncated"));
2141 }
2142 let payload = &self.remaining[KEYFRAME_FRAME_HEADER_LEN..frame_end];
2143 self.remaining = &self.remaining[frame_end..];
2144 Ok(payload)
2145 }
2146
2147 pub fn finish(self) -> CuResult<()> {
2149 if self.remaining.is_empty() {
2150 Ok(())
2151 } else {
2152 Err(CuError::from("Keyframe contains trailing component data"))
2153 }
2154 }
2155}
2156
2157struct FrameSliceReader<'a> {
2158 remaining: &'a [u8],
2159}
2160
2161impl Reader for FrameSliceReader<'_> {
2162 fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> {
2163 if bytes.len() > self.remaining.len() {
2164 return Err(DecodeError::UnexpectedEnd {
2165 additional: bytes.len() - self.remaining.len(),
2166 });
2167 }
2168 let (read, remaining) = self.remaining.split_at(bytes.len());
2169 bytes.copy_from_slice(read);
2170 self.remaining = remaining;
2171 Ok(())
2172 }
2173
2174 fn peek_read(&mut self, length: usize) -> Option<&[u8]> {
2175 self.remaining.get(..length)
2176 }
2177
2178 fn consume(&mut self, length: usize) {
2179 self.remaining = self.remaining.get(length..).unwrap_or_default();
2180 }
2181}
2182
2183#[doc(hidden)]
2185pub fn thaw_keyframe_component(item: &mut impl Freezable, frame: &[u8]) -> CuResult<()> {
2186 let reader = FrameSliceReader { remaining: frame };
2187 let mut decoder = DecoderImpl::new(reader, bincode::config::standard(), ());
2188 item.thaw(&mut decoder)
2189 .map_err(|error| CuError::from(format!("Failed to thaw keyframe component: {error}")))?;
2190 let trailing = decoder.reader().remaining.len();
2191 if trailing != 0 {
2192 return Err(CuError::from(format!(
2193 "Keyframe component snapshot has {} trailing bytes",
2194 trailing
2195 )));
2196 }
2197 Ok(())
2198}
2199
2200#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2202pub enum RuntimeLifecycleConfigSource {
2203 ProgrammaticOverride,
2204 ExternalFile,
2205 BundledDefault,
2206}
2207
2208#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2210pub struct RuntimeLifecycleStackInfo {
2211 pub app_name: String,
2212 pub app_version: String,
2213 pub git_commit: Option<String>,
2214 pub git_dirty: Option<bool>,
2215 pub subsystem_id: Option<String>,
2216 pub subsystem_code: u16,
2217 pub instance_id: u32,
2218}
2219
2220#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2222pub enum RuntimeLifecycleEvent {
2223 Instantiated {
2224 config_source: RuntimeLifecycleConfigSource,
2225 effective_config_ron: String,
2226 stack: RuntimeLifecycleStackInfo,
2227 },
2228 MissionStarted {
2229 mission: String,
2230 },
2231 MissionStopped {
2232 mission: String,
2233 reason: String,
2236 },
2237 Panic {
2239 message: String,
2240 file: Option<String>,
2241 line: Option<u32>,
2242 column: Option<u32>,
2243 },
2244 ShutdownCompleted,
2245}
2246
2247#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2249pub struct RuntimeLifecycleRecord {
2250 pub timestamp: CuTime,
2251 pub event: RuntimeLifecycleEvent,
2252}
2253
2254#[doc(hidden)]
2256pub type RuntimeLifecycleSink = SemanticRecordSink<RuntimeLifecycleRecord>;
2257
2258impl<
2259 CT,
2260 CB,
2261 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2262 M: CuMonitor,
2263 const NBCL: usize,
2264> CuRuntime<CT, CB, P, M, NBCL>
2265{
2266 #[inline]
2270 pub fn record_execution_marker(&self, marker: ExecutionMarker) {
2271 self.execution_probe.record(marker);
2272 }
2273
2274 #[inline]
2279 pub fn execution_probe_ref(&self) -> &RuntimeExecutionProbe {
2280 #[cfg(feature = "std")]
2281 {
2282 self.execution_probe.as_ref()
2283 }
2284
2285 #[cfg(not(feature = "std"))]
2286 {
2287 &self.execution_probe
2288 }
2289 }
2290}
2291
2292#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2297pub enum CuTaskType {
2298 Source,
2299 Regular,
2300 Sink,
2301}
2302
2303impl From<TaskKind> for CuTaskType {
2304 fn from(value: TaskKind) -> Self {
2305 match value {
2306 TaskKind::Source => CuTaskType::Source,
2307 TaskKind::Regular => CuTaskType::Regular,
2308 TaskKind::Sink => CuTaskType::Sink,
2309 }
2310 }
2311}
2312
2313#[derive(Debug, Clone)]
2314pub struct CuOutputPack {
2315 pub culist_index: u32,
2316 pub msg_types: Vec<String>,
2317 pub src_channels: Vec<Option<String>>,
2325}
2326
2327#[derive(Debug, Clone)]
2328pub struct CuInputMsg {
2329 pub culist_index: u32,
2330 pub msg_type: String,
2331 pub src_port: usize,
2332 pub edge_id: usize,
2333 pub connection_order: usize,
2334}
2335
2336#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2344pub enum CuStepPhase {
2345 #[default]
2347 Whole,
2348 AnytimeBase,
2351 AnytimeRefine,
2353}
2354
2355pub struct CuExecutionStep {
2357 pub node_id: NodeId,
2359 pub node: Node,
2361 pub task_type: CuTaskType,
2363 pub phase: CuStepPhase,
2366
2367 pub input_msg_indices_types: Vec<CuInputMsg>,
2370
2371 pub output_msg_pack: Option<CuOutputPack>,
2374}
2375
2376impl Debug for CuExecutionStep {
2377 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
2378 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
2379 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
2380 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
2381 f.write_str(format!(" phase: {:?}\n", self.phase).as_str())?;
2382 f.write_str(
2383 format!(
2384 " input_msg_types: {:?}\n",
2385 self.input_msg_indices_types
2386 )
2387 .as_str(),
2388 )?;
2389 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
2390 Ok(())
2391 }
2392}
2393
2394pub struct CuExecutionLoop {
2399 pub steps: Vec<CuExecutionUnit>,
2400 pub loop_count: Option<u32>,
2401}
2402
2403impl Debug for CuExecutionLoop {
2404 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
2405 f.write_str("CuExecutionLoop:\n")?;
2406 for step in &self.steps {
2407 match step {
2408 CuExecutionUnit::Step(step) => {
2409 step.fmt(f)?;
2410 }
2411 CuExecutionUnit::Loop(l) => {
2412 l.fmt(f)?;
2413 }
2414 }
2415 }
2416
2417 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
2418 Ok(())
2419 }
2420}
2421
2422#[derive(Debug)]
2424pub enum CuExecutionUnit {
2425 Step(Box<CuExecutionStep>),
2426 Loop(CuExecutionLoop),
2427}
2428
2429pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
2430 let node = graph
2431 .get_node(node_id)
2432 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
2433
2434 if node.get_flavor() == crate::config::Flavor::Task {
2435 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
2436 }
2437
2438 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
2439 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
2440 Ok(match (has_inputs, has_outputs) {
2441 (false, true) => CuTaskType::Source,
2442 (true, false) => CuTaskType::Sink,
2443 _ => CuTaskType::Regular,
2444 })
2445}
2446
2447pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
2453 let order = Linearity.plan(graph)?;
2454 check_order(graph, &order)?;
2455 plan_from_order(graph, &order)
2456}
2457
2458pub fn expand_anytime_steps(plan: &mut CuExecutionLoop) -> CuResult<()> {
2476 loop {
2477 let Some(base_pos) = plan.steps.iter().position(|unit| {
2480 matches!(
2481 unit,
2482 CuExecutionUnit::Step(step) if step.phase == CuStepPhase::Whole
2483 && step.node.anytime().is_some()
2484 && !step.node.is_background()
2485 )
2486 }) else {
2487 return Ok(());
2488 };
2489
2490 let CuExecutionUnit::Step(base_step) = &mut plan.steps[base_pos] else {
2491 unreachable!("position() only matches steps");
2492 };
2493 let anytime = base_step
2494 .node
2495 .anytime()
2496 .expect("position() only matches anytime nodes");
2497 let Some(max_refines) = anytime.max_refines else {
2500 return Err(CuError::from(format!(
2501 "Task '{}': a foreground anytime task needs anytime.max_refines to expand into a static plan.",
2502 base_step.node.get_id()
2503 )));
2504 };
2505 base_step.phase = CuStepPhase::AnytimeBase;
2506 let output_pack = base_step.output_msg_pack.clone().ok_or_else(|| {
2507 CuError::from(format!(
2508 "Task '{}': an anytime task needs an output to refine.",
2509 base_step.node.get_id()
2510 ))
2511 })?;
2512 let output_index = output_pack.culist_index;
2513 let node_id = base_step.node_id;
2514 let node = base_step.node.clone();
2515 let task_type = base_step.task_type;
2516
2517 let refine_step = || {
2518 CuExecutionUnit::Step(Box::new(CuExecutionStep {
2519 node_id,
2520 node: node.clone(),
2521 task_type,
2522 phase: CuStepPhase::AnytimeRefine,
2523 input_msg_indices_types: Vec::new(),
2524 output_msg_pack: Some(output_pack.clone()),
2525 }))
2526 };
2527
2528 let consumer_pos = plan.steps[base_pos + 1..]
2531 .iter()
2532 .position(|unit| {
2533 matches!(
2534 unit,
2535 CuExecutionUnit::Step(step) if step
2536 .input_msg_indices_types
2537 .iter()
2538 .any(|input| input.culist_index == output_index)
2539 )
2540 })
2541 .map(|offset| base_pos + 1 + offset)
2542 .unwrap_or(base_pos + 1);
2543
2544 let mut tail = plan.steps.split_off(base_pos + 1);
2545 let suffix = tail.split_off(consumer_pos - base_pos - 1);
2546 let gap = tail;
2547
2548 let mut remaining = max_refines.max(1);
2550 remaining -= 1;
2551 plan.steps.push(refine_step());
2552 for gap_unit in gap {
2553 plan.steps.push(gap_unit);
2554 if remaining > 0 {
2555 remaining -= 1;
2556 plan.steps.push(refine_step());
2557 }
2558 }
2559 for _ in 0..remaining {
2560 plan.steps.push(refine_step());
2561 }
2562 plan.steps.extend(suffix);
2563 }
2564}
2565
2566#[cfg(test)]
2568mod tests {
2569 use super::*;
2570 use crate::config::Node;
2571 use crate::context::CuContext;
2572 use crate::cutask::CuSinkTask;
2573 use crate::cutask::{CuSrcTask, Freezable};
2574 use crate::monitoring::NoMonitor;
2575 use crate::reflect::Reflect;
2576 use bincode::Encode;
2577 use core::cell::Cell;
2578 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
2579 use serde_derive::{Deserialize, Serialize};
2580 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2581 use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
2582 #[cfg(feature = "std")]
2583 use std::sync::{Arc, Mutex};
2584
2585 struct CountingSnapshot<'a> {
2586 calls: &'a Cell<usize>,
2587 value: u32,
2588 fail: bool,
2589 }
2590
2591 impl Freezable for CountingSnapshot<'_> {
2592 fn freeze<E: bincode::enc::Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
2593 self.calls.set(self.calls.get() + 1);
2594 self.value.encode(encoder)?;
2595 if self.fail {
2596 Err(EncodeError::OtherString(
2597 "intentional freeze failure".to_string(),
2598 ))
2599 } else {
2600 Ok(())
2601 }
2602 }
2603 }
2604
2605 #[derive(Default)]
2606 struct SnapshotValue(u32);
2607
2608 impl Freezable for SnapshotValue {
2609 fn thaw<D: bincode::de::Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
2610 self.0 = u32::decode(decoder)?;
2611 Ok(())
2612 }
2613 }
2614
2615 #[test]
2616 fn keyframe_frames_freeze_once_and_roll_back_only_failed_frame() {
2617 let calls = Cell::new(0);
2618 let mut keyframe = KeyFrame::new();
2619 keyframe
2620 .serialized_tasks
2621 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
2622 .unwrap();
2623 keyframe.reset(7, CuTime::from_nanos(70));
2624 keyframe
2625 .add_frozen_task(&CountingSnapshot {
2626 calls: &calls,
2627 value: 11,
2628 fail: false,
2629 })
2630 .unwrap();
2631 let committed_len = keyframe.serialized_tasks.len();
2632
2633 let failing = CountingSnapshot {
2634 calls: &calls,
2635 value: 99,
2636 fail: true,
2637 };
2638 assert!(keyframe.add_frozen_task(&failing).is_err());
2639 assert_eq!(keyframe.serialized_tasks.len(), committed_len);
2640
2641 keyframe
2642 .add_frozen_task(&CountingSnapshot {
2643 calls: &calls,
2644 value: 22,
2645 fail: false,
2646 })
2647 .unwrap();
2648 assert_eq!(calls.get(), 3, "each append must call freeze exactly once");
2649 let first_payload_len = bincode::encode_to_vec(11u32, bincode::config::standard())
2650 .unwrap()
2651 .len();
2652 let second_payload_len = bincode::encode_to_vec(22u32, bincode::config::standard())
2653 .unwrap()
2654 .len();
2655 assert_eq!(
2656 keyframe.serialized_tasks.len(),
2657 KEYFRAME_PAYLOAD_HEADER.len()
2658 + 2 * KEYFRAME_FRAME_HEADER_LEN
2659 + first_payload_len
2660 + second_payload_len,
2661 "component frames carry only a length prefix"
2662 );
2663
2664 let mut frames = KeyFramePayloadReader::new(&keyframe).unwrap();
2665 let mut first = SnapshotValue::default();
2666 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
2667 let mut second = SnapshotValue::default();
2668 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
2669 frames.finish().unwrap();
2670 assert_eq!((first.0, second.0), (11, 22));
2671 }
2672
2673 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2674 #[test]
2675 fn preallocated_keyframe_append_does_not_allocate() {
2676 let calls = Cell::new(0);
2677 let mut keyframe = KeyFrame::new();
2678 keyframe
2679 .serialized_tasks
2680 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
2681 .unwrap();
2682 keyframe.reset(3, CuTime::from_nanos(30));
2683
2684 let allocations = crate::monitoring::ScopedAllocCounter::new();
2685 keyframe
2686 .add_frozen_task(&CountingSnapshot {
2687 calls: &calls,
2688 value: 42,
2689 fail: false,
2690 })
2691 .unwrap();
2692
2693 assert_eq!(allocations.allocated(), 0);
2694 assert_eq!(calls.get(), 1);
2695 }
2696
2697 #[test]
2698 fn keyframe_reader_rejects_legacy_payload_clearly() {
2699 let keyframe = KeyFrame {
2700 culistid: 0,
2701 timestamp: CuTime::default(),
2702 serialized_tasks: vec![0, 1, 2],
2703 };
2704 let error = match KeyFramePayloadReader::new(&keyframe) {
2705 Ok(_) => panic!("legacy payload unexpectedly accepted"),
2706 Err(error) => error,
2707 };
2708 assert!(error.to_string().contains("legacy keyframe payload"));
2709 }
2710
2711 #[derive(Reflect)]
2712 pub struct TestSource {}
2713
2714 impl Freezable for TestSource {}
2715
2716 impl CuSrcTask for TestSource {
2717 type Resources<'r> = ();
2718 type Output<'m> = ();
2719 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2720 where
2721 Self: Sized,
2722 {
2723 Ok(Self {})
2724 }
2725
2726 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
2727 Ok(())
2728 }
2729 }
2730
2731 #[derive(Reflect)]
2732 pub struct TestSink {}
2733
2734 impl Freezable for TestSink {}
2735
2736 impl CuSinkTask for TestSink {
2737 type Resources<'r> = ();
2738 type Input<'m> = ();
2739
2740 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2741 where
2742 Self: Sized,
2743 {
2744 Ok(Self {})
2745 }
2746
2747 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
2748 Ok(())
2749 }
2750 }
2751
2752 type Tasks = (TestSource, TestSink);
2754 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
2755 const TEST_NBCL: usize = 2;
2756
2757 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2758 struct Msgs(());
2759
2760 impl ErasedCuStampedDataSet for Msgs {
2761 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2762 Vec::new()
2763 }
2764 }
2765
2766 impl MatchingTasks for Msgs {
2767 fn get_all_task_ids() -> &'static [&'static str] {
2768 &[]
2769 }
2770 }
2771
2772 impl CuListZeroedInit for Msgs {
2773 fn init_zeroed(&mut self) {}
2774 }
2775
2776 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2777 struct IntMsgs(i32);
2778
2779 impl ErasedCuStampedDataSet for IntMsgs {
2780 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2781 Vec::new()
2782 }
2783 }
2784
2785 impl MatchingTasks for IntMsgs {
2786 fn get_all_task_ids() -> &'static [&'static str] {
2787 &[]
2788 }
2789 }
2790
2791 impl CuListZeroedInit for IntMsgs {
2792 fn init_zeroed(&mut self) {}
2793 }
2794
2795 #[cfg(feature = "std")]
2796 fn tasks_instanciator(
2797 all_instances_configs: Vec<Option<&ComponentConfig>>,
2798 _resources: &mut ResourceManager,
2799 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
2800 ) -> CuResult<Tasks> {
2801 Ok((
2802 TestSource::new(all_instances_configs[0], ())?,
2803 TestSink::new(all_instances_configs[1], ())?,
2804 ))
2805 }
2806
2807 #[cfg(not(feature = "std"))]
2808 fn tasks_instanciator(
2809 all_instances_configs: Vec<Option<&ComponentConfig>>,
2810 _resources: &mut ResourceManager,
2811 ) -> CuResult<Tasks> {
2812 Ok((
2813 TestSource::new(all_instances_configs[0], ())?,
2814 TestSink::new(all_instances_configs[1], ())?,
2815 ))
2816 }
2817
2818 fn monitor_instanciator(
2819 _config: &CuConfig,
2820 metadata: CuMonitoringMetadata,
2821 runtime: CuMonitoringRuntime,
2822 ) -> NoMonitor {
2823 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
2824 }
2825
2826 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
2827 Ok(())
2828 }
2829
2830 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
2831 Ok(ResourceManager::new(&[]))
2832 }
2833
2834 #[derive(Debug)]
2835 struct FakeWriter {}
2836
2837 impl<E: Encode> WriteStream<E> for FakeWriter {
2838 fn log(&mut self, _obj: &E) -> CuResult<()> {
2839 Ok(())
2840 }
2841 }
2842
2843 #[cfg(not(feature = "async-cl-io"))]
2844 #[derive(Debug)]
2845 struct RecordingSyncWriter {
2846 ids: Arc<Mutex<Vec<u64>>>,
2847 last_log_bytes: usize,
2848 fail_on: Option<u64>,
2849 }
2850
2851 #[cfg(not(feature = "async-cl-io"))]
2852 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
2853 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2854 self.ids.lock().unwrap().push(culist.id);
2855 if self.fail_on == Some(culist.id) {
2856 return Err(CuError::from(format!(
2857 "logger failed for CopperList #{}",
2858 culist.id
2859 )));
2860 }
2861 Ok(())
2862 }
2863
2864 fn last_log_bytes(&self) -> Option<usize> {
2865 Some(self.last_log_bytes)
2866 }
2867 }
2868
2869 #[cfg(feature = "std")]
2870 #[derive(Debug)]
2871 struct RecordingSemanticSink {
2872 ids: Arc<Mutex<Vec<u64>>>,
2873 }
2874
2875 #[cfg(feature = "std")]
2876 impl WriteStream<CopperList<IntMsgs>> for RecordingSemanticSink {
2877 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2878 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
2879 self.ids.lock().unwrap().push(culist.id);
2880 Ok(())
2881 }
2882
2883 fn last_log_bytes(&self) -> Option<usize> {
2884 Some(23)
2885 }
2886 }
2887
2888 #[cfg(feature = "std")]
2889 impl WriteStream<CopperList<Msgs>> for RecordingSemanticSink {
2890 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
2891 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
2892 self.ids.lock().unwrap().push(culist.id);
2893 Ok(())
2894 }
2895
2896 fn last_log_bytes(&self) -> Option<usize> {
2897 Some(23)
2898 }
2899 }
2900
2901 #[cfg(feature = "std")]
2902 #[derive(Debug)]
2903 struct RecordingKeyFrameSink {
2904 ids: Arc<Mutex<Vec<u64>>>,
2905 }
2906
2907 #[cfg(feature = "std")]
2908 impl WriteStream<KeyFrame> for RecordingKeyFrameSink {
2909 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
2910 self.ids.lock().unwrap().push(keyframe.culistid);
2911 Ok(())
2912 }
2913
2914 fn last_log_bytes(&self) -> Option<usize> {
2915 Some(29)
2916 }
2917 }
2918
2919 #[test]
2920 fn test_runtime_instantiation() {
2921 let mut config = CuConfig::default();
2922 let graph = config.get_graph_mut(None).unwrap();
2923 graph.add_node(Node::new("a", "TestSource")).unwrap();
2924 graph.add_node(Node::new("b", "TestSink")).unwrap();
2925 graph.connect(0, 1, "()").unwrap();
2926 let runtime: CuResult<TestRuntime> =
2927 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2928 RobotClock::default(),
2929 &config,
2930 crate::config::DEFAULT_MISSION_ID,
2931 CuRuntimeParts::new(
2932 tasks_instanciator,
2933 &[],
2934 &[],
2935 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2936 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2937 monitor_instanciator,
2938 bridges_instanciator,
2939 ),
2940 FakeWriter {},
2941 FakeWriter {},
2942 OutputRequirements::new(true, true),
2943 )
2944 .try_with_resources_instantiator(resources_instanciator)
2945 .and_then(|builder| builder.build());
2946 assert!(runtime.is_ok());
2947 }
2948
2949 #[cfg(feature = "std")]
2950 #[test]
2951 fn downstream_requirements_are_independent_from_local_logging() {
2952 let mut config = CuConfig::default();
2953 config.logging = Some(crate::config::LoggingConfig {
2954 enable_task_logging: false,
2955 enable_keyframe_logging: false,
2956 keyframe_interval: Some(1),
2957 ..Default::default()
2958 });
2959 let graph = config.get_graph_mut(None).unwrap();
2960 graph.add_node(Node::new("a", "TestSource")).unwrap();
2961 graph.add_node(Node::new("b", "TestSink")).unwrap();
2962 graph.connect(0, 1, "()").unwrap();
2963
2964 let copperlist_ids = Arc::new(Mutex::new(Vec::new()));
2965 let keyframe_ids = Arc::new(Mutex::new(Vec::new()));
2966 let mut runtime: TestRuntime =
2967 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2968 RobotClock::default(),
2969 &config,
2970 crate::config::DEFAULT_MISSION_ID,
2971 CuRuntimeParts::new(
2972 tasks_instanciator,
2973 &[],
2974 &[],
2975 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2976 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2977 monitor_instanciator,
2978 bridges_instanciator,
2979 ),
2980 RecordingSemanticSink {
2981 ids: copperlist_ids.clone(),
2982 },
2983 RecordingKeyFrameSink {
2984 ids: keyframe_ids.clone(),
2985 },
2986 OutputRequirements::new(true, false).union(OutputRequirements::new(false, true)),
2987 )
2988 .try_with_resources_instantiator(resources_instanciator)
2989 .and_then(|builder| builder.build())
2990 .unwrap();
2991
2992 let copperlist = runtime.copperlists_manager.create().unwrap();
2993 copperlist.change_state(CopperListState::Processing);
2994 runtime.copperlists_manager.end_of_processing(0).unwrap();
2995 runtime.copperlists_manager.finish_pending().unwrap();
2996
2997 runtime
2998 .keyframes_manager
2999 .try_reset(0, &runtime.clock)
3000 .unwrap();
3001 runtime.keyframes_manager.end_of_processing(0).unwrap();
3002 runtime.keyframes_manager.finish_pending().unwrap();
3003
3004 assert_eq!(*copperlist_ids.lock().unwrap(), vec![0]);
3005 assert_eq!(*keyframe_ids.lock().unwrap(), vec![0]);
3006 }
3007
3008 #[test]
3009 fn test_rate_target_period_rejects_zero() {
3010 let err = rate_target_period(0).expect_err("zero rate target should fail");
3011 assert!(
3012 err.to_string()
3013 .contains("Runtime rate target cannot be zero"),
3014 "unexpected error: {err}"
3015 );
3016 }
3017
3018 #[test]
3019 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
3020 let (clock, mock) = RobotClock::mock();
3021 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3022 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
3023
3024 mock.set_value(10_000_000);
3025 limiter.mark_tick(&clock);
3026
3027 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
3028 }
3029
3030 #[test]
3031 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
3032 let (clock, mock) = RobotClock::mock();
3033 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3034
3035 mock.set_value(35_000_000);
3036 limiter.mark_tick(&clock);
3037
3038 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
3039 }
3040
3041 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
3042 #[test]
3043 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
3044 let (clock, _) = RobotClock::mock();
3045 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
3046 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
3047
3048 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
3049 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
3050 }
3051
3052 #[cfg(not(feature = "async-cl-io"))]
3053 #[test]
3054 fn test_copperlists_manager_lifecycle() {
3055 let mut config = CuConfig::default();
3056 let graph = config.get_graph_mut(None).unwrap();
3057 graph.add_node(Node::new("a", "TestSource")).unwrap();
3058 graph.add_node(Node::new("b", "TestSink")).unwrap();
3059 graph.connect(0, 1, "()").unwrap();
3060
3061 let mut runtime: TestRuntime =
3062 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3063 RobotClock::default(),
3064 &config,
3065 crate::config::DEFAULT_MISSION_ID,
3066 CuRuntimeParts::new(
3067 tasks_instanciator,
3068 &[],
3069 &[],
3070 #[cfg(all(feature = "std", feature = "parallel-rt"))]
3071 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3072 monitor_instanciator,
3073 bridges_instanciator,
3074 ),
3075 FakeWriter {},
3076 FakeWriter {},
3077 OutputRequirements::new(true, true),
3078 )
3079 .try_with_resources_instantiator(resources_instanciator)
3080 .and_then(|builder| builder.build())
3081 .unwrap();
3082
3083 {
3085 let copperlists = &mut runtime.copperlists_manager;
3086 let culist0 = copperlists
3087 .create()
3088 .expect("Ran out of space for copper lists");
3089 let id = culist0.id;
3090 assert_eq!(id, 0);
3091 culist0.change_state(CopperListState::Processing);
3092 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3093 }
3094
3095 {
3096 let copperlists = &mut runtime.copperlists_manager;
3097 let culist1 = copperlists
3098 .create()
3099 .expect("Ran out of space for copper lists");
3100 let id = culist1.id;
3101 assert_eq!(id, 1);
3102 culist1.change_state(CopperListState::Processing);
3103 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3104 }
3105
3106 {
3107 let copperlists = &mut runtime.copperlists_manager;
3108 let culist2 = copperlists.create();
3109 assert!(culist2.is_err());
3110 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3111 let _ = copperlists.end_of_processing(1);
3113 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3114 }
3115
3116 {
3118 let copperlists = &mut runtime.copperlists_manager;
3119 let culist2 = copperlists
3120 .create()
3121 .expect("Ran out of space for copper lists");
3122 let id = culist2.id;
3123 assert_eq!(id, 2);
3124 culist2.change_state(CopperListState::Processing);
3125 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3126 let _ = copperlists.end_of_processing(0);
3128 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3130
3131 let _ = copperlists.end_of_processing(2);
3133 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3136 }
3137 }
3138
3139 #[cfg(not(feature = "async-cl-io"))]
3140 #[test]
3141 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
3142 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3143
3144 assert_eq!(copperlists.next_cl_id(), 0);
3145 assert_eq!(copperlists.last_cl_id(), 0);
3146 assert!(copperlists.peek().is_none());
3147
3148 {
3149 let culist = copperlists.create().unwrap();
3150 culist.msgs.0 = 11;
3151 assert_eq!(culist.id, 0);
3152 assert_eq!(culist.get_state(), CopperListState::Initialized);
3153 }
3154
3155 assert_eq!(copperlists.next_cl_id(), 1);
3156 assert_eq!(copperlists.last_cl_id(), 0);
3157 let peeked = copperlists.peek().unwrap();
3158 assert_eq!(peeked.id, 0);
3159 assert_eq!(peeked.msgs.0, 11);
3160 assert_eq!(peeked.get_state(), CopperListState::Initialized);
3161 }
3162
3163 #[cfg(not(feature = "async-cl-io"))]
3164 #[test]
3165 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
3166 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3167
3168 {
3169 let culist = copperlists.create().unwrap();
3170 culist.msgs.0 = 41;
3171 culist.change_state(CopperListState::Processing);
3172 assert_eq!(culist.id, 0);
3173 }
3174
3175 copperlists.end_of_processing(0).unwrap();
3176 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3177
3178 let reused = copperlists.create().unwrap();
3179 assert_eq!(reused.id, 1);
3180 assert_eq!(reused.get_state(), CopperListState::Initialized);
3181 assert_eq!(reused.msgs.0, 41);
3182 }
3183
3184 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
3185 #[test]
3186 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
3187 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
3188 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3189
3190 {
3191 let culist = copperlists.create().unwrap();
3192 culist.msgs.0 = 10;
3193 culist.change_state(CopperListState::Processing);
3194 }
3195 {
3196 let culist = copperlists.create().unwrap();
3197 culist.msgs.0 = 20;
3198 culist.change_state(CopperListState::Processing);
3199 }
3200
3201 let _ = copperlists.end_of_processing(99);
3202 }
3203
3204 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
3205 #[test]
3206 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
3207 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
3208 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3209
3210 {
3211 let culist = copperlists.create().unwrap();
3212 culist.msgs.0 = 10;
3213 assert_eq!(culist.get_state(), CopperListState::Initialized);
3214 }
3215
3216 let _ = copperlists.end_of_processing(0);
3217 }
3218
3219 #[cfg(not(feature = "async-cl-io"))]
3220 #[test]
3221 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
3222 let ids = Arc::new(Mutex::new(Vec::new()));
3223 let mut copperlists =
3224 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
3225 ids: ids.clone(),
3226 last_log_bytes: 17,
3227 fail_on: None,
3228 })))
3229 .unwrap();
3230
3231 {
3232 let culist = copperlists.create().unwrap();
3233 culist.msgs.0 = 10;
3234 culist.change_state(CopperListState::Processing);
3235 }
3236 {
3237 let culist = copperlists.create().unwrap();
3238 culist.msgs.0 = 20;
3239 culist.change_state(CopperListState::Processing);
3240 }
3241
3242 copperlists.end_of_processing(0).unwrap();
3243 assert!(ids.lock().unwrap().is_empty());
3244 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3245
3246 copperlists.end_of_processing(1).unwrap();
3247
3248 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
3249 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3250 }
3251
3252 #[cfg(not(feature = "async-cl-io"))]
3253 #[test]
3254 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
3255 let ids = Arc::new(Mutex::new(Vec::new()));
3256 let mut copperlists =
3257 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
3258 ids: ids.clone(),
3259 last_log_bytes: 17,
3260 fail_on: None,
3261 })))
3262 .unwrap();
3263 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
3264
3265 {
3266 let culist = copperlists.create().unwrap();
3267 culist.msgs.0 = 10;
3268 culist.change_state(CopperListState::Processing);
3269 }
3270
3271 {
3272 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
3273 capture.select_slot(0);
3274 crate::monitoring::record_payload_handle_bytes(32);
3275 }
3276
3277 copperlists.end_of_processing(0).unwrap();
3278
3279 assert_eq!(*ids.lock().unwrap(), vec![0]);
3280 assert_eq!(copperlists.last_encoded_bytes, 17);
3281 assert_eq!(copperlists.last_handle_bytes, 32);
3282 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3283 }
3284
3285 #[cfg(feature = "std")]
3286 #[test]
3287 fn test_sync_manager_accepts_nonserializing_semantic_sink() {
3288 let ids = Arc::new(Mutex::new(Vec::new()));
3289 let mut copperlists =
3290 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSemanticSink {
3291 ids: ids.clone(),
3292 })))
3293 .unwrap();
3294
3295 let culist = copperlists.create().unwrap();
3296 culist.change_state(CopperListState::Processing);
3297 copperlists.end_of_processing(0).unwrap();
3298
3299 assert_eq!(*ids.lock().unwrap(), vec![0]);
3300 assert_eq!(copperlists.last_encoded_bytes, 23);
3301 assert_eq!(copperlists.last_handle_bytes, 0);
3302 }
3303
3304 #[cfg(feature = "std")]
3305 #[test]
3306 fn test_keyframe_manager_accepts_nonserializing_semantic_sink() {
3307 let ids = Arc::new(Mutex::new(Vec::new()));
3308 let mut keyframes = KeyFramesManager::new(
3309 Some(Box::new(RecordingKeyFrameSink { ids: ids.clone() })),
3310 1,
3311 )
3312 .unwrap();
3313
3314 keyframes.try_reset(7, &RobotClock::default()).unwrap();
3315 keyframes.end_of_processing(7).unwrap();
3316 keyframes.finish_pending().unwrap();
3317
3318 assert_eq!(*ids.lock().unwrap(), vec![7]);
3319 assert_eq!(keyframes.last_encoded_bytes, 29);
3320 }
3321
3322 #[cfg(not(feature = "async-cl-io"))]
3323 #[test]
3324 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
3325 let ids = Arc::new(Mutex::new(Vec::new()));
3326 let mut copperlists =
3327 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
3328 ids: ids.clone(),
3329 last_log_bytes: 17,
3330 fail_on: Some(0),
3331 })))
3332 .unwrap();
3333
3334 {
3335 let culist = copperlists.create().unwrap();
3336 culist.change_state(CopperListState::Processing);
3337 }
3338
3339 let err = copperlists.end_of_processing(0).unwrap_err();
3340
3341 assert!(
3342 err.to_string().contains("logger failed for CopperList #0"),
3343 "unexpected error: {err}"
3344 );
3345 assert_eq!(*ids.lock().unwrap(), vec![0]);
3346 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3347 assert_eq!(copperlists.last_encoded_bytes, 0);
3348 assert_eq!(copperlists.last_handle_bytes, 0);
3349
3350 let peeked = copperlists.peek().unwrap();
3351 assert_eq!(peeked.id, 0);
3352 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
3353 }
3354
3355 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
3356 #[test]
3357 #[should_panic(
3358 expected = "sync boxed end_of_processing expected CopperList #7 to be Processing"
3359 )]
3360 fn test_sync_end_of_processing_boxed_wrong_state_panics_in_debug() {
3361 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3362 let culist = Box::new(CopperList::new(7, IntMsgs::default()));
3363
3364 let _ = copperlists.end_of_processing_boxed(culist);
3365 }
3366
3367 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3368 #[derive(Debug, Default)]
3369 struct RecordingWriter {
3370 ids: Arc<Mutex<Vec<u64>>>,
3371 }
3372
3373 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3374 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
3375 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
3376 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
3377 self.ids.lock().unwrap().push(culist.id);
3378 std::thread::sleep(std::time::Duration::from_millis(2));
3379 Ok(())
3380 }
3381 }
3382
3383 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3384 #[derive(Debug)]
3385 struct BlockingWriter {
3386 ids: Arc<Mutex<Vec<u64>>>,
3387 started: SyncSender<()>,
3388 release: Arc<Mutex<Receiver<()>>>,
3389 }
3390
3391 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3392 impl WriteStream<CopperList<Msgs>> for BlockingWriter {
3393 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
3394 self.ids.lock().unwrap().push(culist.id);
3395 self.started
3396 .send(())
3397 .map_err(|_| CuError::from("failed to signal blocking writer start"))?;
3398 self.release
3399 .lock()
3400 .unwrap()
3401 .recv()
3402 .map_err(|_| CuError::from("failed to release blocking writer"))
3403 }
3404 }
3405
3406 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3407 #[derive(Debug)]
3408 struct BlockingKeyFrameWriter {
3409 ids: Arc<Mutex<Vec<u64>>>,
3410 started: SyncSender<()>,
3411 release: Arc<Mutex<Receiver<()>>>,
3412 }
3413
3414 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3415 impl WriteStream<KeyFrame> for BlockingKeyFrameWriter {
3416 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
3417 self.ids.lock().unwrap().push(keyframe.culistid);
3418 self.started
3419 .send(())
3420 .map_err(|_| CuError::from("failed to signal blocking keyframe writer start"))?;
3421 self.release
3422 .lock()
3423 .unwrap()
3424 .recv()
3425 .map_err(|_| CuError::from("failed to release blocking keyframe writer"))
3426 }
3427 }
3428
3429 #[test]
3430 fn disabled_keyframes_allocate_no_capture_buffers() {
3431 let keyframes = KeyFramesManager::new(None, 1).unwrap();
3432
3433 assert!(keyframes.inner.is_none());
3434 assert!(!keyframes.captures_keyframe(0));
3435 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3436 {
3437 assert!(keyframes.spares.is_empty());
3438 assert!(keyframes.pending_producer.is_none());
3439 assert!(keyframes.worker_handle.is_none());
3440 }
3441 }
3442
3443 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
3444 #[test]
3445 fn disabled_keyframe_manager_allocates_nothing() {
3446 let allocations = crate::monitoring::ScopedAllocCounter::new();
3447 let keyframes = KeyFramesManager::new(None, 1).unwrap();
3448
3449 assert_eq!(allocations.allocated(), 0);
3450 assert!(keyframes.inner.is_none());
3451 }
3452
3453 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3454 #[test]
3455 fn saturated_keyframe_handoff_skips_capture_before_freezing() {
3456 let ids = Arc::new(Mutex::new(Vec::new()));
3457 let (started_tx, started_rx) = sync_channel(1);
3458 let (release_tx, release_rx) = sync_channel(1);
3459 let mut keyframes = KeyFramesManager::new(
3460 Some(Box::new(BlockingKeyFrameWriter {
3461 ids: ids.clone(),
3462 started: started_tx,
3463 release: Arc::new(Mutex::new(release_rx)),
3464 })),
3465 1,
3466 )
3467 .unwrap();
3468 let freeze_calls = Cell::new(0);
3469 let snapshot = CountingSnapshot {
3470 calls: &freeze_calls,
3471 value: 42,
3472 fail: false,
3473 };
3474 keyframes.begin_capture_preallocation();
3475 keyframes.include_capture_capacity(&snapshot).unwrap();
3476 keyframes.finish_capture_preallocation().unwrap();
3477 freeze_calls.set(0);
3478
3479 keyframes.try_reset(0, &RobotClock::default()).unwrap();
3480 assert_ne!(keyframes.freeze_task(0, &snapshot).unwrap(), 0);
3481 keyframes.end_of_processing(0).unwrap();
3482 started_rx
3483 .recv_timeout(std::time::Duration::from_secs(1))
3484 .unwrap();
3485
3486 keyframes.try_reset(1, &RobotClock::default()).unwrap();
3487 assert_ne!(keyframes.freeze_task(1, &snapshot).unwrap(), 0);
3488 keyframes.end_of_processing(1).unwrap();
3489
3490 keyframes.try_reset(2, &RobotClock::default()).unwrap();
3491 assert_eq!(keyframes.freeze_task(2, &snapshot).unwrap(), 0);
3492 keyframes.end_of_processing(2).unwrap();
3493
3494 assert_eq!(freeze_calls.get(), 2);
3495 assert_eq!(keyframes.dropped_keyframes_total(), 1);
3496 assert_eq!(*ids.lock().unwrap(), vec![0]);
3497
3498 release_tx.send(()).unwrap();
3499 started_rx
3500 .recv_timeout(std::time::Duration::from_secs(1))
3501 .unwrap();
3502 release_tx.send(()).unwrap();
3503 keyframes.finish_pending().unwrap();
3504 assert_eq!(*ids.lock().unwrap(), vec![0, 1]);
3505 }
3506
3507 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3508 #[test]
3509 fn test_async_copperlists_manager_flushes_in_order() {
3510 let ids = Arc::new(Mutex::new(Vec::new()));
3511 let mut copperlists = CopperListsManager::<Msgs, 5>::new(Some(Box::new(RecordingWriter {
3512 ids: ids.clone(),
3513 })))
3514 .unwrap();
3515
3516 for expected_id in 0..4 {
3517 let culist = copperlists.create().unwrap();
3518 assert_eq!(culist.id, expected_id);
3519 culist.change_state(CopperListState::Processing);
3520 copperlists.end_of_processing(expected_id).unwrap();
3521 }
3522
3523 copperlists.finish_pending().unwrap();
3524 assert_eq!(copperlists.available_copper_lists().unwrap(), 5);
3525 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
3526 assert_eq!(copperlists.dropped_copperlists_total(), 0);
3527 }
3528
3529 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3530 #[test]
3531 fn test_async_handoff_drops_without_exhausting_execution_slot() {
3532 let ids = Arc::new(Mutex::new(Vec::new()));
3533 let (started_tx, started_rx) = sync_channel(1);
3534 let (release_tx, release_rx) = sync_channel(1);
3535 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
3536 ids: ids.clone(),
3537 started: started_tx,
3538 release: Arc::new(Mutex::new(release_rx)),
3539 })))
3540 .unwrap();
3541
3542 let first = copperlists.create().unwrap();
3543 first.change_state(CopperListState::Processing);
3544 copperlists.end_of_processing(0).unwrap();
3545 started_rx
3546 .recv_timeout(std::time::Duration::from_secs(1))
3547 .unwrap();
3548
3549 let second = copperlists.create().unwrap();
3550 second.change_state(CopperListState::Processing);
3551 copperlists.end_of_processing(1).unwrap();
3552
3553 assert_eq!(copperlists.dropped_copperlists_total(), 1);
3554 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3555 assert_eq!(*ids.lock().unwrap(), vec![0]);
3556
3557 release_tx.send(()).unwrap();
3558 copperlists.finish_pending().unwrap();
3559 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3560 }
3561
3562 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3563 #[test]
3564 fn test_async_boxed_handoff_returns_dropped_copperlist_to_caller() {
3565 let ids = Arc::new(Mutex::new(Vec::new()));
3566 let (started_tx, started_rx) = sync_channel(1);
3567 let (release_tx, release_rx) = sync_channel(1);
3568 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
3569 ids: ids.clone(),
3570 started: started_tx,
3571 release: Arc::new(Mutex::new(release_rx)),
3572 })))
3573 .unwrap();
3574
3575 let mut first = Box::new(CopperList::new(0, Msgs::default()));
3576 first.change_state(CopperListState::Processing);
3577 assert!(matches!(
3578 copperlists.end_of_processing_boxed(first).unwrap(),
3579 OwnedCopperListSubmission::Pending
3580 ));
3581 started_rx
3582 .recv_timeout(std::time::Duration::from_secs(1))
3583 .unwrap();
3584
3585 let mut second = Box::new(CopperList::new(1, Msgs::default()));
3586 second.change_state(CopperListState::Processing);
3587 let recycled = match copperlists.end_of_processing_boxed(second).unwrap() {
3588 OwnedCopperListSubmission::Recycled(culist) => culist,
3589 OwnedCopperListSubmission::Pending => panic!("saturated handoff accepted CopperList"),
3590 };
3591
3592 assert_eq!(recycled.id, 1);
3593 assert_eq!(recycled.get_state(), CopperListState::Free);
3594 assert_eq!(copperlists.dropped_copperlists_total(), 1);
3595 assert_eq!(*ids.lock().unwrap(), vec![0]);
3596
3597 release_tx.send(()).unwrap();
3598 let completed = copperlists.finish_pending_boxed().unwrap();
3599 assert_eq!(completed.len(), 1);
3600 assert_eq!(completed[0].id, 0);
3601 }
3602
3603 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3604 #[test]
3605 fn test_async_output_requires_a_spare_execution_slot() {
3606 let error =
3607 match CopperListsManager::<Msgs, 1>::new(Some(Box::new(RecordingWriter::default()))) {
3608 Ok(_) => panic!("async output unexpectedly accepted a single CopperList slot"),
3609 Err(error) => error,
3610 };
3611
3612 assert!(error.to_string().contains("at least two CopperList slots"));
3613 }
3614
3615 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3616 #[test]
3617 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
3618 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3619
3620 {
3621 let culist = copperlists.create().unwrap();
3622 assert_eq!(culist.id, 0);
3623 assert_eq!(culist.get_state(), CopperListState::Initialized);
3624 culist.msgs.0 = 41;
3625 culist.change_state(CopperListState::Processing);
3626 }
3627
3628 copperlists.end_of_processing(0).unwrap();
3629 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3630
3631 let reused = copperlists.create().unwrap();
3632 assert_eq!(reused.id, 1);
3633 assert_eq!(reused.get_state(), CopperListState::Initialized);
3634 assert_eq!(reused.msgs.0, 41);
3635 }
3636
3637 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
3638 #[test]
3639 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
3640 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
3641 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3642
3643 let culist = copperlists.create().unwrap();
3644 assert_eq!(culist.id, 0);
3645 assert_eq!(culist.get_state(), CopperListState::Initialized);
3646
3647 let _ = copperlists.end_of_processing(0);
3648 }
3649
3650 #[test]
3651 fn test_runtime_task_input_order() {
3652 let mut config = CuConfig::default();
3653 let graph = config.get_graph_mut(None).unwrap();
3654 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
3655 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
3656 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
3657
3658 assert_eq!(src1_id, 0);
3659 assert_eq!(src2_id, 1);
3660
3661 let src1_type = "src1_type";
3663 let src2_type = "src2_type";
3664 graph.connect(src2_id, sink_id, src2_type).unwrap();
3665 graph.connect(src1_id, sink_id, src1_type).unwrap();
3666
3667 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
3668 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
3669 assert_eq!(src1_edge_id, 1);
3672 assert_eq!(src2_edge_id, 0);
3673
3674 let runtime = compute_runtime_plan(graph).unwrap();
3675 let sink_step = runtime
3676 .steps
3677 .iter()
3678 .find_map(|step| match step {
3679 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
3680 _ => None,
3681 })
3682 .unwrap();
3683
3684 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
3687 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
3688 }
3689
3690 #[test]
3691 fn test_runtime_output_ports_unique_ordered() {
3692 let mut config = CuConfig::default();
3693 let graph = config.get_graph_mut(None).unwrap();
3694 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3695 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
3696 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
3697 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
3698 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
3699
3700 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
3701 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
3702 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
3703 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
3704
3705 let runtime = compute_runtime_plan(graph).unwrap();
3706 let src_step = runtime
3707 .steps
3708 .iter()
3709 .find_map(|step| match step {
3710 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3711 _ => None,
3712 })
3713 .unwrap();
3714
3715 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3716 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
3717
3718 let dst_a_step = runtime
3719 .steps
3720 .iter()
3721 .find_map(|step| match step {
3722 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
3723 _ => None,
3724 })
3725 .unwrap();
3726 let dst_b_step = runtime
3727 .steps
3728 .iter()
3729 .find_map(|step| match step {
3730 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
3731 _ => None,
3732 })
3733 .unwrap();
3734 let dst_a2_step = runtime
3735 .steps
3736 .iter()
3737 .find_map(|step| match step {
3738 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
3739 _ => None,
3740 })
3741 .unwrap();
3742 let dst_c_step = runtime
3743 .steps
3744 .iter()
3745 .find_map(|step| match step {
3746 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
3747 _ => None,
3748 })
3749 .unwrap();
3750
3751 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
3752 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
3753 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
3754 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
3755 }
3756
3757 #[test]
3758 fn test_runtime_plan_distinguishes_channel_distinct_outputs() {
3759 let mut config = CuConfig::default();
3760 let graph = config.get_graph_mut(None).unwrap();
3761 let src_id = graph.add_node(Node::new("cam", "Cam")).unwrap();
3762 let sink_id = graph.add_node(Node::new("sink", "Sink")).unwrap();
3763
3764 graph
3768 .connect_ext(
3769 src_id,
3770 sink_id,
3771 "msg::Image",
3772 None,
3773 Some("left".to_string()),
3774 None,
3775 )
3776 .unwrap();
3777 graph
3778 .connect_ext(
3779 src_id,
3780 sink_id,
3781 "msg::Image",
3782 None,
3783 Some("right".to_string()),
3784 None,
3785 )
3786 .unwrap();
3787
3788 let runtime = compute_runtime_plan(graph).unwrap();
3789
3790 let src_step = runtime
3791 .steps
3792 .iter()
3793 .find_map(|step| match step {
3794 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3795 _ => None,
3796 })
3797 .unwrap();
3798 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3799 assert_eq!(output_pack.msg_types, vec!["msg::Image", "msg::Image"]);
3800 assert_eq!(
3801 output_pack.src_channels,
3802 vec![Some("left".to_string()), Some("right".to_string())]
3803 );
3804
3805 let sink_step = runtime
3806 .steps
3807 .iter()
3808 .find_map(|step| match step {
3809 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
3810 _ => None,
3811 })
3812 .unwrap();
3813
3814 assert_eq!(sink_step.input_msg_indices_types.len(), 2);
3815 let ports: Vec<usize> = sink_step
3816 .input_msg_indices_types
3817 .iter()
3818 .map(|input| input.src_port)
3819 .collect();
3820 assert_eq!(ports, vec![0, 1]);
3821 }
3822
3823 #[test]
3824 fn test_runtime_output_ports_fanout_single() {
3825 let mut config = CuConfig::default();
3826 let graph = config.get_graph_mut(None).unwrap();
3827 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3828 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
3829 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
3830
3831 graph.connect(src_id, dst_a_id, "i32").unwrap();
3832 graph.connect(src_id, dst_b_id, "i32").unwrap();
3833
3834 let runtime = compute_runtime_plan(graph).unwrap();
3835 let src_step = runtime
3836 .steps
3837 .iter()
3838 .find_map(|step| match step {
3839 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3840 _ => None,
3841 })
3842 .unwrap();
3843
3844 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3845 assert_eq!(output_pack.msg_types, vec!["i32"]);
3846 }
3847
3848 #[test]
3849 fn test_runtime_output_ports_include_nc_outputs() {
3850 let mut config = CuConfig::default();
3851 let graph = config.get_graph_mut(None).unwrap();
3852 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3853 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
3854 graph.connect(src_id, dst_id, "msg::A").unwrap();
3855 graph
3856 .get_node_mut(src_id)
3857 .expect("missing source node")
3858 .add_nc_output("msg::B", usize::MAX);
3859
3860 let runtime = compute_runtime_plan(graph).unwrap();
3861 let src_step = runtime
3862 .steps
3863 .iter()
3864 .find_map(|step| match step {
3865 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3866 _ => None,
3867 })
3868 .unwrap();
3869 let dst_step = runtime
3870 .steps
3871 .iter()
3872 .find_map(|step| match step {
3873 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3874 _ => None,
3875 })
3876 .unwrap();
3877
3878 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3879 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3880 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
3881 }
3882
3883 #[test]
3884 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
3885 let txt = r#"(
3886 tasks: [
3887 (id: "src", type: "a"),
3888 (id: "regular", type: "b"),
3889 ],
3890 cnx: [
3891 (src: "src", dst: "regular", msg: "msg::A"),
3892 (src: "regular", dst: "__nc__", msg: "msg::B"),
3893 ]
3894 )"#;
3895 let config = CuConfig::deserialize_ron(txt).unwrap();
3896 let graph = config.get_graph(None).unwrap();
3897 let regular_id = graph.get_node_id_by_name("regular").unwrap();
3898
3899 let runtime = compute_runtime_plan(graph).unwrap();
3900 let regular_step = runtime
3901 .steps
3902 .iter()
3903 .find_map(|step| match step {
3904 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
3905 _ => None,
3906 })
3907 .unwrap();
3908
3909 assert_eq!(regular_step.task_type, CuTaskType::Regular);
3910 assert_eq!(
3911 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
3912 vec!["msg::B"]
3913 );
3914 }
3915
3916 #[test]
3917 fn test_runtime_output_ports_respect_connection_order_with_nc() {
3918 let txt = r#"(
3919 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3920 cnx: [
3921 (src: "src", dst: "__nc__", msg: "msg::A"),
3922 (src: "src", dst: "sink", msg: "msg::B"),
3923 ]
3924 )"#;
3925 let config = CuConfig::deserialize_ron(txt).unwrap();
3926 let graph = config.get_graph(None).unwrap();
3927 let src_id = graph.get_node_id_by_name("src").unwrap();
3928 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3929
3930 let runtime = compute_runtime_plan(graph).unwrap();
3931 let src_step = runtime
3932 .steps
3933 .iter()
3934 .find_map(|step| match step {
3935 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3936 _ => None,
3937 })
3938 .unwrap();
3939 let dst_step = runtime
3940 .steps
3941 .iter()
3942 .find_map(|step| match step {
3943 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3944 _ => None,
3945 })
3946 .unwrap();
3947
3948 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3949 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3950 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3951 }
3952
3953 #[cfg(feature = "std")]
3954 #[test]
3955 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
3956 let txt = r#"(
3957 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3958 cnx: [
3959 (src: "src", dst: "__nc__", msg: "msg::A"),
3960 (src: "src", dst: "sink", msg: "msg::B"),
3961 ]
3962 )"#;
3963 let tmp = tempfile::NamedTempFile::new().unwrap();
3964 std::fs::write(tmp.path(), txt).unwrap();
3965 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
3966 let graph = config.get_graph(None).unwrap();
3967 let src_id = graph.get_node_id_by_name("src").unwrap();
3968 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3969
3970 let runtime = compute_runtime_plan(graph).unwrap();
3971 let src_step = runtime
3972 .steps
3973 .iter()
3974 .find_map(|step| match step {
3975 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3976 _ => None,
3977 })
3978 .unwrap();
3979 let dst_step = runtime
3980 .steps
3981 .iter()
3982 .find_map(|step| match step {
3983 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3984 _ => None,
3985 })
3986 .unwrap();
3987
3988 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3989 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3990 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3991 }
3992
3993 #[test]
3994 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
3995 let txt = r#"(
3996 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3997 cnx: [
3998 (src: "src", dst: "__nc__", msg: "i32"),
3999 (src: "src", dst: "sink", msg: "bool"),
4000 ]
4001 )"#;
4002 let config = CuConfig::deserialize_ron(txt).unwrap();
4003 let graph = config.get_graph(None).unwrap();
4004 let src_id = graph.get_node_id_by_name("src").unwrap();
4005 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4006
4007 let runtime = compute_runtime_plan(graph).unwrap();
4008 let src_step = runtime
4009 .steps
4010 .iter()
4011 .find_map(|step| match step {
4012 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4013 _ => None,
4014 })
4015 .unwrap();
4016 let dst_step = runtime
4017 .steps
4018 .iter()
4019 .find_map(|step| match step {
4020 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4021 _ => None,
4022 })
4023 .unwrap();
4024
4025 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4026 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
4027 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4028 }
4029
4030 #[test]
4031 fn test_runtime_plan_diamond_case1() {
4032 let mut config = CuConfig::default();
4034 let graph = config.get_graph_mut(None).unwrap();
4035 let cam0_id = graph
4036 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
4037 .unwrap();
4038 let inf0_id = graph
4039 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
4040 .unwrap();
4041 let broadcast_id = graph
4042 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
4043 .unwrap();
4044
4045 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
4047 graph.connect(cam0_id, inf0_id, "i32").unwrap();
4048 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
4049
4050 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
4051 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
4052
4053 assert_eq!(edge_cam0_to_inf0, 0);
4054 assert_eq!(edge_cam0_to_broadcast, 1);
4055
4056 let runtime = compute_runtime_plan(graph).unwrap();
4057 let broadcast_step = runtime
4058 .steps
4059 .iter()
4060 .find_map(|step| match step {
4061 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
4062 _ => None,
4063 })
4064 .unwrap();
4065
4066 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
4067 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
4068 }
4069
4070 #[test]
4071 fn test_runtime_plan_diamond_case2() {
4072 let mut config = CuConfig::default();
4074 let graph = config.get_graph_mut(None).unwrap();
4075 let cam0_id = graph
4076 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
4077 .unwrap();
4078 let inf0_id = graph
4079 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
4080 .unwrap();
4081 let broadcast_id = graph
4082 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
4083 .unwrap();
4084
4085 graph.connect(cam0_id, inf0_id, "i32").unwrap();
4087 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
4088 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
4089
4090 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
4091 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
4092
4093 assert_eq!(edge_cam0_to_broadcast, 0);
4094 assert_eq!(edge_cam0_to_inf0, 1);
4095
4096 let runtime = compute_runtime_plan(graph).unwrap();
4097 let broadcast_step = runtime
4098 .steps
4099 .iter()
4100 .find_map(|step| match step {
4101 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
4102 _ => None,
4103 })
4104 .unwrap();
4105
4106 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
4107 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
4108 }
4109
4110 use crate::config::AnytimeConfig;
4113
4114 fn anytime_node(id: &str, max_refines: Option<u32>) -> Node {
4115 let mut node = Node::new(id, "tasks::AnytimeTask");
4116 node.set_anytime(Some(AnytimeConfig {
4117 max_refines,
4118 ..Default::default()
4119 }));
4120 node
4121 }
4122
4123 fn plan_shape(plan: &CuExecutionLoop) -> Vec<(NodeId, CuStepPhase)> {
4125 plan.steps
4126 .iter()
4127 .map(|unit| match unit {
4128 CuExecutionUnit::Step(step) => (step.node_id, step.phase),
4129 CuExecutionUnit::Loop(_) => panic!("no loops expected"),
4130 })
4131 .collect()
4132 }
4133
4134 fn manual_step(node: Node, node_id: NodeId, inputs: &[u32], output: u32) -> CuExecutionUnit {
4137 CuExecutionUnit::Step(Box::new(CuExecutionStep {
4138 node_id,
4139 node,
4140 task_type: CuTaskType::Regular,
4141 phase: CuStepPhase::default(),
4142 input_msg_indices_types: inputs
4143 .iter()
4144 .map(|&culist_index| CuInputMsg {
4145 culist_index,
4146 msg_type: "msg::A".to_string(),
4147 src_port: 0,
4148 edge_id: 0,
4149 connection_order: 0,
4150 })
4151 .collect(),
4152 output_msg_pack: Some(CuOutputPack {
4153 culist_index: output,
4154 msg_types: vec!["msg::A".to_string()],
4155 src_channels: vec![None],
4156 }),
4157 }))
4158 }
4159
4160 #[test]
4161 fn test_anytime_expansion_contiguous_without_gap() {
4162 let mut config = CuConfig::default();
4165 let graph = config.get_graph_mut(None).unwrap();
4166 let src_id = graph.add_node(Node::new("src", "tasks::Src")).unwrap();
4167 let any_id = graph.add_node(anytime_node("any", Some(3))).unwrap();
4168 let sink_id = graph.add_node(Node::new("sink", "tasks::Sink")).unwrap();
4169 graph.connect(src_id, any_id, "msg::A").unwrap();
4170 graph.connect(any_id, sink_id, "msg::B").unwrap();
4171
4172 let mut plan = compute_runtime_plan(graph).unwrap();
4173 expand_anytime_steps(&mut plan).unwrap();
4174
4175 assert_eq!(
4176 plan_shape(&plan),
4177 vec![
4178 (src_id, CuStepPhase::Whole),
4179 (any_id, CuStepPhase::AnytimeBase),
4180 (any_id, CuStepPhase::AnytimeRefine),
4181 (any_id, CuStepPhase::AnytimeRefine),
4182 (any_id, CuStepPhase::AnytimeRefine),
4183 (sink_id, CuStepPhase::Whole),
4184 ]
4185 );
4186
4187 let (base_pack, refine_steps): (Option<CuOutputPack>, Vec<&CuExecutionStep>) = {
4189 let mut base_pack = None;
4190 let mut refines = Vec::new();
4191 for unit in &plan.steps {
4192 if let CuExecutionUnit::Step(step) = unit {
4193 match step.phase {
4194 CuStepPhase::AnytimeBase => base_pack = step.output_msg_pack.clone(),
4195 CuStepPhase::AnytimeRefine => refines.push(step.as_ref()),
4196 CuStepPhase::Whole => {}
4197 }
4198 }
4199 }
4200 (base_pack, refines)
4201 };
4202 let base_pack = base_pack.unwrap();
4203 for refine in refine_steps {
4204 assert!(refine.input_msg_indices_types.is_empty());
4205 let pack = refine.output_msg_pack.as_ref().unwrap();
4206 assert_eq!(pack.culist_index, base_pack.culist_index);
4207 }
4208 }
4209
4210 #[test]
4211 fn test_anytime_expansion_interleaves_with_gap_steps() {
4212 let mut plan = CuExecutionLoop {
4215 steps: vec![
4216 manual_step(anytime_node("any", Some(4)), 0, &[], 0),
4217 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
4218 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
4219 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
4220 ],
4221 loop_count: None,
4222 };
4223 expand_anytime_steps(&mut plan).unwrap();
4224 assert_eq!(
4225 plan_shape(&plan),
4226 vec![
4227 (0, CuStepPhase::AnytimeBase),
4228 (0, CuStepPhase::AnytimeRefine),
4229 (1, CuStepPhase::Whole),
4230 (0, CuStepPhase::AnytimeRefine),
4231 (2, CuStepPhase::Whole),
4232 (0, CuStepPhase::AnytimeRefine),
4233 (0, CuStepPhase::AnytimeRefine),
4234 (3, CuStepPhase::Whole),
4235 ]
4236 );
4237 }
4238
4239 #[test]
4240 fn test_anytime_expansion_fewer_refines_than_gaps() {
4241 let mut plan = CuExecutionLoop {
4243 steps: vec![
4244 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
4245 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
4246 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
4247 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
4248 ],
4249 loop_count: None,
4250 };
4251 expand_anytime_steps(&mut plan).unwrap();
4252 assert_eq!(
4253 plan_shape(&plan),
4254 vec![
4255 (0, CuStepPhase::AnytimeBase),
4256 (0, CuStepPhase::AnytimeRefine),
4257 (1, CuStepPhase::Whole),
4258 (0, CuStepPhase::AnytimeRefine),
4259 (2, CuStepPhase::Whole),
4260 (3, CuStepPhase::Whole),
4261 ]
4262 );
4263 }
4264
4265 #[test]
4266 fn test_anytime_expansion_without_consumer() {
4267 let mut plan = CuExecutionLoop {
4269 steps: vec![
4270 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
4271 manual_step(Node::new("other", "t"), 1, &[], 1),
4272 ],
4273 loop_count: None,
4274 };
4275 expand_anytime_steps(&mut plan).unwrap();
4276 assert_eq!(
4277 plan_shape(&plan),
4278 vec![
4279 (0, CuStepPhase::AnytimeBase),
4280 (0, CuStepPhase::AnytimeRefine),
4281 (0, CuStepPhase::AnytimeRefine),
4282 (1, CuStepPhase::Whole),
4283 ]
4284 );
4285 }
4286
4287 #[test]
4288 fn test_anytime_expansion_two_nodes_interleave() {
4289 let mut plan = CuExecutionLoop {
4292 steps: vec![
4293 manual_step(anytime_node("any_a", Some(2)), 0, &[], 0),
4294 manual_step(anytime_node("any_b", Some(2)), 1, &[], 1),
4295 manual_step(Node::new("consumer", "t"), 2, &[0, 1], 2),
4296 ],
4297 loop_count: None,
4298 };
4299 expand_anytime_steps(&mut plan).unwrap();
4300 assert_eq!(
4303 plan_shape(&plan),
4304 vec![
4305 (0, CuStepPhase::AnytimeBase),
4306 (0, CuStepPhase::AnytimeRefine),
4307 (1, CuStepPhase::AnytimeBase),
4308 (1, CuStepPhase::AnytimeRefine),
4309 (0, CuStepPhase::AnytimeRefine),
4310 (1, CuStepPhase::AnytimeRefine),
4311 (2, CuStepPhase::Whole),
4312 ]
4313 );
4314 }
4315
4316 #[test]
4317 fn test_anytime_expansion_requires_max_refines() {
4318 let mut plan = CuExecutionLoop {
4319 steps: vec![
4320 manual_step(anytime_node("any", None), 0, &[], 0),
4321 manual_step(Node::new("consumer", "t"), 1, &[0], 1),
4322 ],
4323 loop_count: None,
4324 };
4325 let err = expand_anytime_steps(&mut plan).unwrap_err();
4326 assert!(err.to_string().contains("needs anytime.max_refines"));
4327 }
4328}