1use crate::app::Subsystem;
6#[cfg(feature = "std")]
7use crate::arena::{CuSlotLease, allocate_slots};
8use crate::config::{ComponentConfig, DEFAULT_KEYFRAME_INTERVAL, Node, TaskKind};
9use crate::config::{
10 CuConfig, CuGraph, MAX_RATE_TARGET_HZ, NodeId, RuntimeConfig, resolve_task_kind_for_id,
11};
12use crate::copperlist::{CopperList, CopperListState, CuListZeroedInit, CuListsManager};
13use crate::cutask::{BincodeAdapter, Freezable};
14#[cfg(feature = "std")]
15use crate::monitoring::ExecutionProbeHandle;
16#[cfg(feature = "std")]
17use crate::monitoring::MonitorExecutionProbe;
18use crate::monitoring::{
19 ComponentId, CopperListInfo, CuMonitor, CuMonitoringMetadata, CuMonitoringRuntime,
20 ExecutionMarker, MonitorComponentMetadata, RuntimeExecutionProbe, build_monitor_topology,
21 take_last_completed_handle_bytes,
22};
23#[cfg(all(feature = "std", feature = "parallel-rt"))]
24use crate::parallel_rt::ParallelRt;
25#[cfg(feature = "std")]
26use crate::parallel_rt::ParallelRtMetadata;
27use crate::planner::{check_order, plan_from_order, topo_bfs_order};
28use crate::resource::ResourceManager;
29#[cfg(feature = "std")]
30use alloc::sync::Arc;
31use compact_str::CompactString;
32use cu29_clock::{ClockProvider, CuDuration, CuTime, RobotClock};
33use cu29_traits::CuResult;
34use cu29_traits::WriteStream;
35use cu29_traits::{CopperListTuple, CuError};
36#[cfg(feature = "std")]
37use rayon::ThreadPool;
38
39#[cfg(target_os = "none")]
40#[allow(unused_imports)]
41use cu29_log::{ANONYMOUS, CuLogEntry, CuLogLevel};
42#[cfg(target_os = "none")]
43#[allow(unused_imports)]
44use cu29_log_derive::info;
45#[cfg(target_os = "none")]
46#[allow(unused_imports)]
47use cu29_log_runtime::log;
48#[cfg(all(target_os = "none", debug_assertions))]
49#[allow(unused_imports)]
50use cu29_log_runtime::log_debug_mode;
51#[cfg(target_os = "none")]
52#[allow(unused_imports)]
53use cu29_value::to_value;
54
55use alloc::boxed::Box;
56use alloc::format;
57use alloc::string::{String, ToString};
58use alloc::vec::Vec;
59use bincode::de::read::Reader;
60use bincode::de::{Decoder, DecoderImpl};
61use bincode::enc::EncoderImpl;
62use bincode::enc::write::{SizeWriter, Writer};
63use bincode::error::{DecodeError, EncodeError};
64use bincode::{Decode, Encode};
65use core::fmt::Result as FmtResult;
66use core::fmt::{Debug, Formatter};
67use core::marker::PhantomData;
68
69#[cfg(all(feature = "std", feature = "async-cl-io"))]
70use rtrb::{Consumer, PopError, Producer, PushError, RingBuffer};
71#[cfg(all(feature = "std", feature = "async-cl-io"))]
72use std::sync::atomic::{AtomicBool, Ordering};
73#[cfg(all(feature = "std", feature = "async-cl-io"))]
74use std::thread::{JoinHandle, Thread};
75
76#[cfg(feature = "std")]
77#[doc(hidden)]
78pub type TasksInstantiator<CT> = for<'c> fn(
79 Vec<Option<&'c ComponentConfig>>,
80 &mut ResourceManager,
81 &[Option<Arc<ThreadPool>>],
82) -> CuResult<CT>;
83#[cfg(not(feature = "std"))]
84#[doc(hidden)]
85pub type TasksInstantiator<CT> =
86 for<'c> fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>;
87#[doc(hidden)]
88pub type BridgesInstantiator<CB> = fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>;
89#[cfg(feature = "std")]
96#[doc(hidden)]
97pub type ThreadPoolsInstantiator = fn(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>;
98#[doc(hidden)]
99pub type MonitorInstantiator<M> = fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M;
100
101#[doc(hidden)]
102pub struct CuRuntimeParts<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI> {
103 pub tasks_instanciator: TI,
104 pub monitored_components: &'static [MonitorComponentMetadata],
105 pub culist_component_mapping: &'static [ComponentId],
106 #[cfg(feature = "std")]
107 pub parallel_rt_metadata: &'static ParallelRtMetadata,
108 pub monitor_instanciator: MI,
109 pub bridges_instanciator: BI,
110 _payload: PhantomData<(CT, CB, P, M, [(); NBCL])>,
111}
112
113impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI>
114 CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>
115{
116 pub const fn new(
117 tasks_instanciator: TI,
118 monitored_components: &'static [MonitorComponentMetadata],
119 culist_component_mapping: &'static [ComponentId],
120 #[cfg(feature = "std")] parallel_rt_metadata: &'static ParallelRtMetadata,
121 monitor_instanciator: MI,
122 bridges_instanciator: BI,
123 ) -> Self {
124 Self {
125 tasks_instanciator,
126 monitored_components,
127 culist_component_mapping,
128 #[cfg(feature = "std")]
129 parallel_rt_metadata,
130 monitor_instanciator,
131 bridges_instanciator,
132 _payload: PhantomData,
133 }
134 }
135}
136
137#[doc(hidden)]
138pub struct CuRuntimeBuilder<
139 'cfg,
140 CT,
141 CB,
142 P: CopperListTuple,
143 M: CuMonitor,
144 const NBCL: usize,
145 TI,
146 BI,
147 MI,
148 CLS,
149 KFS,
150> {
151 clock: RobotClock,
152 config: &'cfg CuConfig,
153 mission: &'cfg str,
154 subsystem: Subsystem,
155 instance_id: u32,
156 resources: Option<ResourceManager>,
157 #[cfg(feature = "std")]
158 thread_pools: Option<Vec<Option<Arc<ThreadPool>>>>,
159 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
160 copperlist_sink: CLS,
161 keyframe_sink: KFS,
162 output_requirements: OutputRequirements,
163}
164
165impl<'cfg, CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI, CLS, KFS>
166 CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
167{
168 pub fn new(
169 clock: RobotClock,
170 config: &'cfg CuConfig,
171 mission: &'cfg str,
172 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
173 copperlist_sink: CLS,
174 keyframe_sink: KFS,
175 output_requirements: OutputRequirements,
176 ) -> Self {
177 Self {
178 clock,
179 config,
180 mission,
181 subsystem: Subsystem::new(None, 0),
182 instance_id: 0,
183 resources: None,
184 #[cfg(feature = "std")]
185 thread_pools: None,
186 parts,
187 copperlist_sink,
188 keyframe_sink,
189 output_requirements,
190 }
191 }
192
193 pub fn with_subsystem(mut self, subsystem: Subsystem) -> Self {
194 self.subsystem = subsystem;
195 self
196 }
197
198 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
199 self.instance_id = instance_id;
200 self
201 }
202
203 pub fn with_resources(mut self, resources: ResourceManager) -> Self {
204 self.resources = Some(resources);
205 self
206 }
207
208 pub fn try_with_resources_instantiator(
209 mut self,
210 resources_instantiator: impl FnOnce(&CuConfig) -> CuResult<ResourceManager>,
211 ) -> CuResult<Self> {
212 self.resources = Some(resources_instantiator(self.config)?);
213 Ok(self)
214 }
215
216 #[cfg(feature = "std")]
220 pub fn with_thread_pools(mut self, pools: Vec<Option<Arc<ThreadPool>>>) -> Self {
221 self.thread_pools = Some(pools);
222 self
223 }
224
225 #[cfg(feature = "std")]
226 pub fn try_with_thread_pools_instantiator(
227 mut self,
228 thread_pools_instantiator: impl FnOnce(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>,
229 ) -> CuResult<Self> {
230 self.thread_pools = Some(thread_pools_instantiator(self.config)?);
231 Ok(self)
232 }
233}
234
235#[inline]
246pub fn perf_now(_clock: &RobotClock) -> CuTime {
247 #[cfg(all(feature = "std", feature = "sysclock-perf"))]
248 {
249 static PERF_CLOCK: std::sync::OnceLock<RobotClock> = std::sync::OnceLock::new();
250 return PERF_CLOCK.get_or_init(RobotClock::new).now();
251 }
252
253 #[allow(unreachable_code)]
254 _clock.now()
255}
256
257#[cfg(all(feature = "std", feature = "high-precision-limiter"))]
258const HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS: u64 = 200_000;
259
260#[inline]
262pub fn rate_target_period(rate_target_hz: u64) -> CuResult<CuDuration> {
263 if rate_target_hz == 0 {
264 return Err(CuError::from(
265 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
266 ));
267 }
268
269 if rate_target_hz > MAX_RATE_TARGET_HZ {
270 return Err(CuError::from(format!(
271 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
272 )));
273 }
274
275 Ok(CuDuration::from(MAX_RATE_TARGET_HZ / rate_target_hz))
276}
277
278#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285pub struct LoopRateLimiter {
286 period: CuDuration,
287 next_deadline: CuTime,
288}
289
290impl LoopRateLimiter {
291 #[inline]
292 pub fn from_rate_target_hz(rate_target_hz: u64, clock: &RobotClock) -> CuResult<Self> {
293 let period = rate_target_period(rate_target_hz)?;
294 Ok(Self {
295 period,
296 next_deadline: clock.now() + period,
297 })
298 }
299
300 #[inline]
301 pub fn is_ready(&self, clock: &RobotClock) -> bool {
302 self.remaining(clock).is_none()
303 }
304
305 #[inline]
306 pub fn remaining(&self, clock: &RobotClock) -> Option<CuDuration> {
307 let now = clock.now();
308 if now < self.next_deadline {
309 Some(self.next_deadline - now)
310 } else {
311 None
312 }
313 }
314
315 #[inline]
316 pub fn wait_until_ready(&self, clock: &RobotClock) {
317 let deadline = self.next_deadline;
318 let Some(remaining) = self.remaining(clock) else {
319 return;
320 };
321
322 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
323 {
324 let spin_window = self.spin_window();
325 if remaining > spin_window {
326 std::thread::sleep(std::time::Duration::from(remaining - spin_window));
327 }
328 while clock.now() < deadline {
329 core::hint::spin_loop();
330 }
331 }
332
333 #[cfg(all(feature = "std", not(feature = "high-precision-limiter")))]
334 {
335 let _ = deadline;
336 std::thread::sleep(std::time::Duration::from(remaining));
337 }
338
339 #[cfg(not(feature = "std"))]
340 {
341 let _ = remaining;
342 while clock.now() < deadline {
343 core::hint::spin_loop();
344 }
345 }
346 }
347
348 #[inline]
349 pub fn mark_tick(&mut self, clock: &RobotClock) {
350 self.advance_from(clock.now());
351 }
352
353 #[inline]
354 pub fn limit(&mut self, clock: &RobotClock) {
355 self.wait_until_ready(clock);
356 self.mark_tick(clock);
357 }
358
359 #[inline]
360 fn advance_from(&mut self, now: CuTime) {
361 let steps = if now < self.next_deadline {
362 1
363 } else {
364 (now - self.next_deadline).as_nanos() / self.period.as_nanos() + 1
365 };
366 self.next_deadline += steps * self.period;
367 }
368
369 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
370 #[inline]
371 fn spin_window(&self) -> CuDuration {
372 let _ = self.period;
373 CuDuration::from(HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS)
374 }
375
376 #[cfg(test)]
377 #[inline]
378 fn next_deadline(&self) -> CuTime {
379 self.next_deadline
380 }
381}
382
383#[cfg(all(feature = "std", feature = "async-cl-io"))]
384#[doc(hidden)]
385pub trait AsyncCopperListPayload: Send {}
386
387#[cfg(all(feature = "std", feature = "async-cl-io"))]
388impl<T: Send> AsyncCopperListPayload for T {}
389
390#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
391#[doc(hidden)]
392pub trait AsyncCopperListPayload {}
393
394#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
395impl<T> AsyncCopperListPayload for T {}
396
397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
404#[doc(hidden)]
405pub enum ProcessStepOutcome {
406 Continue,
407 AbortCopperList,
408}
409
410#[doc(hidden)]
412pub type ProcessStepResult = CuResult<ProcessStepOutcome>;
413
414#[cfg(feature = "remote-debug")]
415fn encode_completed_copperlist_snapshot<P: CopperListTuple>(
416 cl: &CopperList<P>,
417) -> CuResult<Vec<u8>> {
418 bincode::encode_to_vec(cl, bincode::config::standard())
419 .map_err(|e| CuError::new_with_cause("Failed to encode completed CopperList snapshot", e))
420}
421
422#[doc(hidden)]
428pub type SemanticRecordSink<T> = dyn WriteStream<T>;
429
430#[doc(hidden)]
432pub type CompletedCopperListSink<P> = SemanticRecordSink<CopperList<P>>;
433
434#[doc(hidden)]
436pub type CompletedKeyFrameSink = SemanticRecordSink<KeyFrame>;
437
438#[derive(Clone, Copy, Debug, Default)]
440#[doc(hidden)]
441pub struct NullWriteStream;
442
443impl<E: Encode> WriteStream<E> for NullWriteStream {
444 #[inline]
445 fn log(&mut self, _record: &E) -> CuResult<()> {
446 Ok(())
447 }
448}
449
450#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
456#[doc(hidden)]
457pub struct OutputRequirements {
458 completed_copperlists: bool,
459 keyframes: bool,
460}
461
462impl OutputRequirements {
463 #[inline]
464 pub const fn new(completed_copperlists: bool, keyframes: bool) -> Self {
465 Self {
466 completed_copperlists,
467 keyframes,
468 }
469 }
470
471 #[inline]
472 pub const fn completed_copperlists(self) -> bool {
473 self.completed_copperlists
474 }
475
476 #[inline]
477 pub const fn keyframes(self) -> bool {
478 self.keyframes
479 }
480
481 #[inline]
483 pub const fn union(self, other: Self) -> Self {
484 Self::new(
485 self.completed_copperlists || other.completed_copperlists,
486 self.keyframes || other.keyframes,
487 )
488 }
489}
490
491#[doc(hidden)]
493pub struct SyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
494 inner: Option<CuListsManager<P, NBCL>>,
495 #[cfg(feature = "std")]
496 free_pool: Vec<CuSlotLease<P>>,
497 #[cfg(feature = "std")]
498 keyframe_sink: Option<Box<CompletedKeyFrameSink>>,
499 #[cfg(feature = "std")]
500 next_distributed_id: u64,
501 #[cfg(feature = "std")]
502 pub last_keyframe_bytes: u64,
503 sink: Option<Box<CompletedCopperListSink<P>>>,
504 #[cfg(feature = "remote-debug")]
506 last_completed_encoded: Option<Vec<u8>>,
507 pub last_encoded_bytes: u64,
509 pub last_handle_bytes: u64,
511}
512
513impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
514 #[doc(hidden)]
515 pub fn close_output(&mut self) -> CuResult<()> {
516 self.finish_pending()?;
517 if let Some(sink) = &mut self.sink {
518 sink.flush()?;
519 }
520 self.sink.take();
521 #[cfg(feature = "std")]
522 {
523 if let Some(sink) = &mut self.keyframe_sink {
524 sink.flush()?;
525 }
526 self.keyframe_sink.take();
527 }
528 Ok(())
529 }
530
531 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
532 where
533 P: CuListZeroedInit,
534 {
535 Self::new_with_keyframes(sink, None)
536 }
537
538 pub fn new_with_keyframes(
539 sink: Option<Box<CompletedCopperListSink<P>>>,
540 keyframe_sink: Option<Box<CompletedKeyFrameSink>>,
541 ) -> CuResult<Self>
542 where
543 P: CuListZeroedInit,
544 {
545 #[cfg(not(feature = "std"))]
546 let _ = keyframe_sink;
547 Ok(Self {
548 inner: (!P::DISTRIBUTED).then(CuListsManager::new),
549 #[cfg(feature = "std")]
550 free_pool: if P::DISTRIBUTED {
551 allocate_slots::<P>(NBCL)
552 } else {
553 Vec::new()
554 },
555 #[cfg(feature = "std")]
556 keyframe_sink,
557 #[cfg(feature = "std")]
558 next_distributed_id: 0,
559 #[cfg(feature = "std")]
560 last_keyframe_bytes: 0,
561 sink,
562 #[cfg(feature = "remote-debug")]
563 last_completed_encoded: None,
564 last_encoded_bytes: 0,
565 last_handle_bytes: 0,
566 })
567 }
568
569 pub fn next_cl_id(&self) -> u64 {
570 #[cfg(feature = "std")]
571 {
572 self.inner
573 .as_ref()
574 .map_or(self.next_distributed_id, CuListsManager::next_cl_id)
575 }
576 #[cfg(not(feature = "std"))]
577 {
578 self.inner.as_ref().expect("serial storage").next_cl_id()
579 }
580 }
581
582 #[cfg(feature = "std")]
583 pub fn note_admission(&mut self, next_id: u64) {
584 self.next_distributed_id = self.next_distributed_id.max(next_id);
585 }
586
587 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
590 if !self.inner.as_ref().is_none_or(CuListsManager::is_empty) {
591 return Err(CuError::from(
592 "Cannot reposition replay with active CopperLists",
593 ));
594 }
595 if let Some(inner) = &mut self.inner {
596 inner.set_next_replay_id(next_id);
597 }
598 #[cfg(feature = "std")]
599 {
600 self.next_distributed_id = next_id;
601 }
602 Ok(())
603 }
604
605 pub fn last_cl_id(&self) -> u64 {
606 self.next_cl_id().saturating_sub(1)
607 }
608
609 pub fn peek(&self) -> Option<&CopperList<P>> {
610 self.inner.as_ref().and_then(CuListsManager::peek)
611 }
612
613 #[cfg(feature = "remote-debug")]
614 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
615 self.last_completed_encoded.as_deref()
616 }
617
618 #[cfg(not(feature = "remote-debug"))]
619 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
620 None
621 }
622
623 #[cfg(feature = "remote-debug")]
624 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
625 self.last_completed_encoded = snapshot;
626 }
627
628 #[cfg(not(feature = "remote-debug"))]
629 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
630
631 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
632 where
633 P: CuListZeroedInit,
634 {
635 self.inner
636 .as_mut()
637 .expect("serial storage")
638 .create()
639 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))
640 }
641
642 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
643 #[cfg(debug_assertions)]
644 self.debug_assert_end_of_processing_target(culistid);
645
646 let mut is_top = true;
647 let mut nb_done = 0;
648 self.last_encoded_bytes = 0;
649 self.last_handle_bytes = 0;
650 #[cfg(feature = "remote-debug")]
651 let last_completed_encoded = &mut self.last_completed_encoded;
652 for cl in self.inner.as_mut().expect("serial storage").iter_mut() {
653 if cl.id == culistid && cl.get_state() == CopperListState::Processing {
654 cl.change_state(CopperListState::DoneProcessing);
655 #[cfg(feature = "remote-debug")]
656 {
657 *last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
658 }
659 }
660 if is_top && cl.get_state() == CopperListState::DoneProcessing {
661 if let Some(sink) = &mut self.sink {
662 cl.change_state(CopperListState::BeingSerialized);
663 sink.log(cl)?;
664 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
665 self.last_handle_bytes = take_last_completed_handle_bytes();
666 }
667 cl.change_state(CopperListState::Free);
668 nb_done += 1;
669 } else {
670 is_top = false;
671 }
672 }
673 for _ in 0..nb_done {
674 let _ = self.inner.as_mut().expect("serial storage").pop();
675 }
676 Ok(())
677 }
678
679 pub fn finish_pending(&mut self) -> CuResult<()> {
680 Ok(())
681 }
682
683 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
684 if let Some(inner) = &self.inner {
685 return Ok(NBCL - inner.len());
686 }
687 #[cfg(feature = "std")]
688 {
689 Ok(self.free_pool.len())
690 }
691 #[cfg(not(feature = "std"))]
692 {
693 Ok(0)
694 }
695 }
696
697 #[inline]
698 pub const fn dropped_copperlists_total(&self) -> u64 {
699 0
700 }
701
702 #[cfg(feature = "std")]
703 pub fn submit_slot(
704 &mut self,
705 mut culist: CuSlotLease<P>,
706 ) -> CuResult<OwnedCopperListSubmission<P>> {
707 #[cfg(debug_assertions)]
708 debug_assert_processing_completion_state(culist.as_ref(), "sync slot completion");
709
710 self.note_admission(culist.id + 1);
711 culist.change_state(CopperListState::DoneProcessing);
712 self.last_keyframe_bytes = 0;
713 if let Some(sink) = &mut self.keyframe_sink
714 && let Some(keyframe) = culist.keyframe().filter(|keyframe| keyframe.is_active())
715 {
716 sink.log(keyframe)?;
717 self.last_keyframe_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
718 }
719 self.last_encoded_bytes = 0;
720 self.last_handle_bytes = 0;
721 if let Some(sink) = &mut self.sink {
722 culist.change_state(CopperListState::BeingSerialized);
723 sink.log(&culist)?;
724 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
725 self.last_handle_bytes = take_last_completed_handle_bytes();
726 }
727 culist.change_state(CopperListState::Free);
728 Ok(OwnedCopperListSubmission::Recycled(culist))
729 }
730
731 #[cfg(feature = "std")]
732 pub fn try_reclaim_slot(&mut self) -> CuResult<Option<CuSlotLease<P>>> {
733 Ok(None)
734 }
735
736 #[cfg(feature = "std")]
737 pub const fn has_pending_slots(&self) -> bool {
738 false
739 }
740
741 #[cfg(feature = "std")]
742 pub fn wait_reclaim_slot(&mut self) -> CuResult<CuSlotLease<P>> {
743 Err(CuError::from(
744 "Synchronous CopperList I/O cannot block waiting for slot completions",
745 ))
746 }
747
748 #[cfg(feature = "std")]
749 pub fn finish_pending_slots(&mut self) -> CuResult<Vec<CuSlotLease<P>>> {
750 Ok(Vec::new())
751 }
752
753 #[cfg(feature = "std")]
754 pub fn finish_pending_with(&mut self, _reclaim: impl FnMut(CuSlotLease<P>)) -> CuResult<()> {
755 Ok(())
756 }
757
758 #[cfg(debug_assertions)]
759 fn debug_assert_end_of_processing_target(&self, culistid: u64) {
760 let mut matches = 0usize;
761 let mut state = None;
762 for cl in self.inner.as_ref().expect("serial storage").iter() {
763 if cl.id == culistid {
764 matches += 1;
765 state = Some(cl.get_state());
766 }
767 }
768
769 assert_eq!(
770 matches, 1,
771 "sync end_of_processing expected exactly one active CopperList #{culistid}, found {matches}"
772 );
773 assert_eq!(
774 state,
775 Some(CopperListState::Processing),
776 "sync end_of_processing expected CopperList #{culistid} to be Processing, found {:?}",
777 state
778 );
779 }
780}
781
782#[cfg(feature = "std")]
783impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
784 pub fn acquire_iteration_slot(&mut self) -> CuResult<CuSlotLease<P>> {
785 self.free_pool
786 .pop()
787 .ok_or_else(|| CuError::from("CopperList arena has no free slot"))
788 }
789
790 pub fn prepare_keyframes(&mut self, capacities: &[usize]) {
791 if !capacities.is_empty()
792 && let Some(slot) = self.free_pool.first_mut()
793 {
794 slot.prepare_keyframes(capacities);
795 }
796 }
797
798 pub fn recycle_slot(&mut self, slot: CuSlotLease<P>) {
799 self.free_pool.push(slot);
800 }
801
802 pub fn take_execution_slots(&mut self) -> Vec<CuSlotLease<P>> {
803 core::mem::take(&mut self.free_pool)
804 }
805
806 pub fn return_execution_slots(&mut self, slots: Vec<CuSlotLease<P>>) {
807 self.free_pool = slots;
808 }
809}
810
811#[cfg(feature = "std")]
813#[doc(hidden)]
814pub enum OwnedCopperListSubmission<P: CopperListTuple> {
815 Recycled(CuSlotLease<P>),
817 Pending,
819}
820
821#[cfg(all(feature = "std", feature = "async-cl-io"))]
822struct AsyncCopperListCompletion<P: CopperListTuple> {
823 culist: CuSlotLease<P>,
824 sink_result: CuResult<(u64, u64, u64)>,
825 #[cfg(feature = "remote-debug")]
826 completed_snapshot: CuResult<Vec<u8>>,
827}
828
829#[cfg(all(feature = "std", feature = "async-cl-io"))]
830struct AsyncOutputWorkerRunningGuard(Arc<AtomicBool>);
831
832#[cfg(all(feature = "std", feature = "async-cl-io"))]
833impl Drop for AsyncOutputWorkerRunningGuard {
834 fn drop(&mut self) {
835 self.0.store(false, Ordering::Release);
836 }
837}
838
839#[cfg(all(feature = "std", feature = "async-cl-io"))]
841#[doc(hidden)]
842pub struct AsyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
843 free_pool: Vec<CuSlotLease<P>>,
844 current: Option<CuSlotLease<P>>,
845 #[cfg(feature = "remote-debug")]
846 last_completed_encoded: Option<Vec<u8>>,
847 pending_count: usize,
848 next_cl_id: u64,
849 pending_producer: Option<Producer<CuSlotLease<P>>>,
850 completion_consumer: Option<Consumer<AsyncCopperListCompletion<P>>>,
851 worker_handle: Option<JoinHandle<()>>,
852 worker_thread: Option<Thread>,
853 worker_shutdown: Option<Arc<AtomicBool>>,
854 worker_running: Option<Arc<AtomicBool>>,
855 dropped_copperlists_total: u64,
856 pub last_encoded_bytes: u64,
858 pub last_handle_bytes: u64,
860 pub last_keyframe_bytes: u64,
861 distributed: bool,
862}
863
864#[cfg(all(feature = "std", feature = "async-cl-io"))]
865impl<P: CopperListTuple + Default, const NBCL: usize> AsyncCopperListsManager<P, NBCL> {
866 #[doc(hidden)]
867 pub fn close_output(&mut self) -> CuResult<()> {
868 self.shutdown_worker()
869 }
870
871 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
872 where
873 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
874 {
875 Self::new_with_keyframes(sink, None)
876 }
877
878 pub fn new_with_keyframes(
879 sink: Option<Box<CompletedCopperListSink<P>>>,
880 keyframe_sink: Option<Box<CompletedKeyFrameSink>>,
881 ) -> CuResult<Self>
882 where
883 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
884 {
885 let free_pool = allocate_slots::<P>(NBCL);
886
887 if sink.is_some() && !P::DISTRIBUTED && NBCL < 2 {
888 return Err(CuError::from(
889 "async CopperList output requires at least two CopperList slots",
890 ));
891 }
892
893 let (
894 pending_producer,
895 completion_consumer,
896 worker_handle,
897 worker_thread,
898 worker_shutdown,
899 worker_running,
900 ) = if sink.is_some() || keyframe_sink.is_some() {
901 let mut sink = sink;
902 let mut keyframe_sink = keyframe_sink;
903 let handoff_capacity = NBCL.max(1);
904 let (pending_producer, mut pending_consumer) =
905 RingBuffer::<CuSlotLease<P>>::new(handoff_capacity);
906 let (mut completion_producer, completion_consumer) =
907 RingBuffer::<AsyncCopperListCompletion<P>>::new(handoff_capacity);
908 let worker_shutdown = Arc::new(AtomicBool::new(false));
909 let worker_running = Arc::new(AtomicBool::new(true));
910 let shutdown = worker_shutdown.clone();
911 let running = worker_running.clone();
912 let worker_handle = std::thread::Builder::new()
913 .name("cu-async-cl-io".to_string())
914 .spawn(move || {
915 let _running_guard = AsyncOutputWorkerRunningGuard(running);
916 loop {
917 let mut culist = match pending_consumer.pop() {
918 Ok(culist) => culist,
919 Err(PopError::Empty) => {
920 if shutdown.load(Ordering::Acquire) {
921 break;
922 }
923 std::thread::park();
924 continue;
925 }
926 };
927 #[cfg(feature = "remote-debug")]
928 let completed_snapshot = {
929 culist.change_state(CopperListState::DoneProcessing);
932 encode_completed_copperlist_snapshot(&culist)
933 };
934 culist.change_state(CopperListState::BeingSerialized);
935 let sink_result = (|| {
936 let mut keyframe_bytes = 0;
937 if let Some(keyframe_sink) = &mut keyframe_sink
938 && let Some(keyframe) =
939 culist.keyframe().filter(|keyframe| keyframe.is_active())
940 {
941 keyframe_sink.log(keyframe)?;
942 keyframe_bytes = keyframe_sink.last_log_bytes().unwrap_or(0) as u64;
943 }
944 if let Some(sink) = &mut sink {
945 sink.log(&culist)?;
946 Ok((
947 sink.last_log_bytes().unwrap_or(0) as u64,
948 take_last_completed_handle_bytes(),
949 keyframe_bytes,
950 ))
951 } else {
952 Ok((0, 0, keyframe_bytes))
953 }
954 })();
955 let should_stop = sink_result.is_err();
956 #[cfg(feature = "remote-debug")]
957 let should_stop = should_stop || completed_snapshot.is_err();
958 let mut completion = AsyncCopperListCompletion {
959 culist,
960 sink_result,
961 #[cfg(feature = "remote-debug")]
962 completed_snapshot,
963 };
964 loop {
965 match completion_producer.push(completion) {
966 Ok(()) => break,
967 Err(PushError::Full(returned)) => {
968 completion = returned;
969 std::thread::yield_now();
970 }
971 }
972 }
973 if should_stop {
974 break;
975 }
976 }
977 })
978 .map_err(|e| {
979 CuError::from("Failed to spawn async CopperList serializer thread")
980 .add_cause(e.to_string().as_str())
981 })?;
982 let worker_thread = worker_handle.thread().clone();
983 (
984 Some(pending_producer),
985 Some(completion_consumer),
986 Some(worker_handle),
987 Some(worker_thread),
988 Some(worker_shutdown),
989 Some(worker_running),
990 )
991 } else {
992 (None, None, None, None, None, None)
993 };
994
995 Ok(Self {
996 free_pool,
997 current: None,
998 #[cfg(feature = "remote-debug")]
999 last_completed_encoded: None,
1000 pending_count: 0,
1001 next_cl_id: 0,
1002 pending_producer,
1003 completion_consumer,
1004 worker_handle,
1005 worker_thread,
1006 worker_shutdown,
1007 worker_running,
1008 dropped_copperlists_total: 0,
1009 last_encoded_bytes: 0,
1010 last_handle_bytes: 0,
1011 last_keyframe_bytes: 0,
1012 distributed: P::DISTRIBUTED,
1013 })
1014 }
1015
1016 pub fn acquire_iteration_slot(&mut self) -> CuResult<CuSlotLease<P>> {
1017 self.reclaim_completed()?;
1018 match self.free_pool.pop() {
1019 Some(slot) => Ok(slot),
1020 None => self.wait_reclaim_slot(),
1021 }
1022 }
1023
1024 pub fn prepare_keyframes(&mut self, capacities: &[usize]) {
1025 if !capacities.is_empty()
1026 && let Some(slot) = self.free_pool.first_mut()
1027 {
1028 slot.prepare_keyframes(capacities);
1029 }
1030 }
1031
1032 pub fn recycle_slot(&mut self, slot: CuSlotLease<P>) {
1033 self.free_pool.push(slot);
1034 }
1035
1036 pub fn take_execution_slots(&mut self) -> Vec<CuSlotLease<P>> {
1037 core::mem::take(&mut self.free_pool)
1038 }
1039
1040 pub fn return_execution_slots(&mut self, slots: Vec<CuSlotLease<P>>) {
1041 self.free_pool = slots;
1042 }
1043
1044 pub fn note_admission(&mut self, next_id: u64) {
1045 self.next_cl_id = self.next_cl_id.max(next_id);
1046 }
1047
1048 pub fn next_cl_id(&self) -> u64 {
1049 self.next_cl_id
1050 }
1051
1052 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
1055 self.finish_pending()?;
1056 self.next_cl_id = next_id;
1057 Ok(())
1058 }
1059
1060 pub fn last_cl_id(&self) -> u64 {
1061 self.next_cl_id.saturating_sub(1)
1062 }
1063
1064 pub fn peek(&self) -> Option<&CopperList<P>> {
1065 self.current.as_deref()
1066 }
1067
1068 #[cfg(feature = "remote-debug")]
1069 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
1070 self.last_completed_encoded.as_deref()
1071 }
1072
1073 #[cfg(not(feature = "remote-debug"))]
1074 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
1075 None
1076 }
1077
1078 #[cfg(feature = "remote-debug")]
1079 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
1080 self.last_completed_encoded = snapshot;
1081 }
1082
1083 #[cfg(not(feature = "remote-debug"))]
1084 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
1085
1086 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
1087 where
1088 P: CuListZeroedInit,
1089 {
1090 if self.current.is_some() {
1091 return Err(CuError::from(
1092 "Attempted to create a CopperList while another one is still active",
1093 ));
1094 }
1095
1096 self.reclaim_completed()?;
1097
1098 let culist = self.free_pool.pop().ok_or_else(|| {
1099 CuError::from("CopperList output handoff exhausted the slot reserved for execution")
1100 })?;
1101 self.current = Some(culist);
1102
1103 let current = self
1104 .current
1105 .as_mut()
1106 .expect("current CopperList is missing");
1107 current.reset(self.next_cl_id);
1108 self.next_cl_id += 1;
1109 Ok(current.as_mut())
1110 }
1111
1112 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1113 self.reclaim_completed()?;
1114
1115 let mut culist = self.current.take().ok_or_else(|| {
1116 CuError::from("Attempted to finish processing without an active CopperList")
1117 })?;
1118
1119 if culist.id != culistid {
1120 return Err(CuError::from(format!(
1121 "Attempted to finish CopperList #{culistid} while CopperList #{} is active",
1122 culist.id
1123 )));
1124 }
1125 #[cfg(debug_assertions)]
1126 debug_assert_processing_completion_state(culist.as_ref(), "async end_of_processing");
1127
1128 culist.change_state(CopperListState::DoneProcessing);
1129 self.last_encoded_bytes = 0;
1130 self.last_handle_bytes = 0;
1131
1132 match self.try_submit(culist)? {
1133 OwnedCopperListSubmission::Recycled(culist) => self.free_pool.push(culist),
1134 OwnedCopperListSubmission::Pending => {}
1135 }
1136
1137 Ok(())
1138 }
1139
1140 pub fn finish_pending(&mut self) -> CuResult<()> {
1141 if self.current.is_some() {
1142 return Err(CuError::from(
1143 "Cannot flush CopperList I/O while a CopperList is still active",
1144 ));
1145 }
1146
1147 while self.pending_count > 0 {
1148 self.wait_for_completion()?;
1149 }
1150 Ok(())
1151 }
1152
1153 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
1154 self.reclaim_completed()?;
1155 Ok(self.free_pool.len())
1156 }
1157
1158 #[inline]
1159 pub const fn dropped_copperlists_total(&self) -> u64 {
1160 self.dropped_copperlists_total
1161 }
1162
1163 pub fn submit_slot(
1164 &mut self,
1165 mut culist: CuSlotLease<P>,
1166 ) -> CuResult<OwnedCopperListSubmission<P>> {
1167 #[cfg(debug_assertions)]
1168 debug_assert_processing_completion_state(culist.as_ref(), "async slot completion");
1169 self.next_cl_id = self.next_cl_id.max(culist.id + 1);
1170 culist.change_state(CopperListState::DoneProcessing);
1171 self.last_encoded_bytes = 0;
1172 self.last_handle_bytes = 0;
1173
1174 self.try_submit(culist)
1175 }
1176
1177 fn try_submit(&mut self, mut culist: CuSlotLease<P>) -> CuResult<OwnedCopperListSubmission<P>> {
1178 let Some(pending_producer) = self.pending_producer.as_mut() else {
1179 culist.change_state(CopperListState::Free);
1180 return Ok(OwnedCopperListSubmission::Recycled(culist));
1181 };
1182
1183 if !self.distributed && self.pending_count >= NBCL - 1 {
1187 return Ok(self.drop_copperlist(culist));
1188 }
1189
1190 culist.change_state(CopperListState::QueuedForSerialization);
1191 match pending_producer.push(culist) {
1192 Ok(()) => {
1193 self.pending_count += 1;
1194 if let Some(worker_thread) = self.worker_thread.as_ref() {
1195 worker_thread.unpark();
1196 }
1197 Ok(OwnedCopperListSubmission::Pending)
1198 }
1199 Err(PushError::Full(culist)) => Ok(self.drop_copperlist(culist)),
1200 }
1201 }
1202
1203 fn drop_copperlist(&mut self, mut culist: CuSlotLease<P>) -> OwnedCopperListSubmission<P> {
1204 self.dropped_copperlists_total = self.dropped_copperlists_total.saturating_add(1);
1205 culist.change_state(CopperListState::Free);
1206 OwnedCopperListSubmission::Recycled(culist)
1207 }
1208
1209 pub fn try_reclaim_slot(&mut self) -> CuResult<Option<CuSlotLease<P>>> {
1210 let pop_result = {
1211 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1212 return Ok(None);
1213 };
1214 completion_consumer.pop()
1215 };
1216 match pop_result {
1217 Ok(completion) => self.handle_completion(completion).map(Some),
1218 Err(PopError::Empty) => {
1219 if self.pending_count > 0
1220 && self
1221 .worker_running
1222 .as_ref()
1223 .is_some_and(|running| !running.load(Ordering::Acquire))
1224 {
1225 Err(CuError::from(
1226 "Async CopperList output worker stopped unexpectedly",
1227 ))
1228 } else {
1229 Ok(None)
1230 }
1231 }
1232 }
1233 }
1234
1235 pub fn has_pending_slots(&self) -> bool {
1236 self.pending_count > 0
1237 }
1238
1239 pub fn wait_reclaim_slot(&mut self) -> CuResult<CuSlotLease<P>> {
1240 if self.completion_consumer.is_none() {
1241 return Err(CuError::from(
1242 "No async CopperList output worker is active to return a free slot",
1243 ));
1244 }
1245 loop {
1246 if let Some(culist) = self.try_reclaim_slot()? {
1247 return Ok(culist);
1248 }
1249 std::thread::yield_now();
1250 }
1251 }
1252
1253 pub fn finish_pending_slots(&mut self) -> CuResult<Vec<CuSlotLease<P>>> {
1254 let mut reclaimed = Vec::with_capacity(self.pending_count);
1255 if self.current.is_some() {
1256 return Err(CuError::from(
1257 "Cannot flush CopperList I/O while a CopperList is still active",
1258 ));
1259 }
1260 while self.pending_count > 0 {
1261 reclaimed.push(self.wait_reclaim_slot()?);
1262 }
1263 Ok(reclaimed)
1264 }
1265
1266 pub fn finish_pending_with(&mut self, mut reclaim: impl FnMut(CuSlotLease<P>)) -> CuResult<()> {
1267 if self.current.is_some() {
1268 return Err(CuError::from(
1269 "Cannot flush CopperList I/O while a CopperList is still active",
1270 ));
1271 }
1272 while self.pending_count > 0 {
1273 reclaim(self.wait_reclaim_slot()?);
1274 }
1275 Ok(())
1276 }
1277
1278 fn reclaim_completed(&mut self) -> CuResult<()> {
1279 loop {
1280 let Some(culist) = self.try_reclaim_slot()? else {
1281 break;
1282 };
1283 self.free_pool.push(culist);
1284 }
1285 Ok(())
1286 }
1287
1288 fn wait_for_completion(&mut self) -> CuResult<()> {
1289 let culist = self.wait_reclaim_slot()?;
1290 self.free_pool.push(culist);
1291 Ok(())
1292 }
1293
1294 fn handle_completion(
1295 &mut self,
1296 mut completion: AsyncCopperListCompletion<P>,
1297 ) -> CuResult<CuSlotLease<P>> {
1298 self.pending_count = self.pending_count.saturating_sub(1);
1299 if let Ok((encoded_bytes, handle_bytes, keyframe_bytes)) = completion.sink_result.as_ref() {
1300 self.last_encoded_bytes = *encoded_bytes;
1301 self.last_handle_bytes = *handle_bytes;
1302 self.last_keyframe_bytes = *keyframe_bytes;
1303 }
1304 completion.culist.change_state(CopperListState::Free);
1305 completion.sink_result?;
1306 #[cfg(feature = "remote-debug")]
1307 {
1308 self.last_completed_encoded = Some(completion.completed_snapshot?);
1309 }
1310 Ok(completion.culist)
1311 }
1312
1313 fn shutdown_worker(&mut self) -> CuResult<()> {
1314 self.finish_pending()?;
1315 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1316 shutdown.store(true, Ordering::Release);
1317 }
1318 if let Some(worker_thread) = self.worker_thread.as_ref() {
1319 worker_thread.unpark();
1320 }
1321 if let Some(worker_handle) = self.worker_handle.take() {
1322 worker_handle.join().map_err(|_| {
1323 CuError::from("Async CopperList output worker panicked while joining")
1324 })?;
1325 }
1326 self.pending_producer.take();
1327 self.worker_thread.take();
1328 self.worker_shutdown.take();
1329 self.worker_running.take();
1330 Ok(())
1331 }
1332}
1333
1334#[cfg(all(feature = "std", feature = "async-cl-io"))]
1335impl<P: CopperListTuple + Default, const NBCL: usize> Drop for AsyncCopperListsManager<P, NBCL> {
1336 fn drop(&mut self) {
1337 let _ = self.shutdown_worker();
1338 }
1339}
1340
1341#[cfg(all(feature = "std", debug_assertions))]
1342fn debug_assert_processing_completion_state<P: CopperListTuple>(
1343 culist: &CopperList<P>,
1344 context: &str,
1345) {
1346 assert_eq!(
1347 culist.get_state(),
1348 CopperListState::Processing,
1349 "{context} expected CopperList #{} to be Processing, found {}",
1350 culist.id,
1351 culist.get_state()
1352 );
1353}
1354
1355#[cfg(all(feature = "std", feature = "async-cl-io"))]
1356#[doc(hidden)]
1357pub type CopperListsManager<P, const NBCL: usize> = AsyncCopperListsManager<P, NBCL>;
1358
1359#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1360#[doc(hidden)]
1361pub type CopperListsManager<P, const NBCL: usize> = SyncCopperListsManager<P, NBCL>;
1362
1363#[cfg(all(feature = "std", feature = "async-cl-io"))]
1364struct AsyncKeyFrameCompletion {
1365 keyframe: Box<KeyFrame>,
1366 sink_result: CuResult<u64>,
1367}
1368
1369pub struct KeyFramesManager {
1371 distributed: bool,
1372 #[doc(hidden)]
1374 pub capture_capacities: Vec<usize>,
1375 inner: Option<KeyFrame>,
1377
1378 forced_timestamp: Option<CuTime>,
1380
1381 locked: bool,
1383
1384 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1386 sink: Option<Box<CompletedKeyFrameSink>>,
1387
1388 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1392 #[allow(clippy::vec_box)]
1393 spares: Vec<Box<KeyFrame>>,
1394 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1395 pending_count: usize,
1396 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1397 pending_producer: Option<Producer<Box<KeyFrame>>>,
1398 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1399 completion_consumer: Option<Consumer<AsyncKeyFrameCompletion>>,
1400 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1401 worker_handle: Option<JoinHandle<()>>,
1402 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1403 worker_thread: Option<Thread>,
1404 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1405 worker_shutdown: Option<Arc<AtomicBool>>,
1406 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1407 worker_running: Option<Arc<AtomicBool>>,
1408 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1409 capture_this_copperlist: Option<u64>,
1410 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1411 dropped_keyframes_total: u64,
1412
1413 keyframe_interval: u32,
1415
1416 pub last_encoded_bytes: u64,
1418
1419 capture_size_hint: usize,
1421}
1422
1423const MIN_KEYFRAME_CAPTURE_CAPACITY: usize = 4 * 1024;
1424#[cfg(all(feature = "std", feature = "async-cl-io"))]
1425const ASYNC_KEYFRAME_HANDOFF_CAPACITY: usize = 2;
1426
1427struct PreallocatedVecWriter<'a>(&'a mut Vec<u8>);
1429
1430impl Writer for PreallocatedVecWriter<'_> {
1431 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
1432 if bytes.len() > self.0.capacity().saturating_sub(self.0.len()) {
1433 return Err(EncodeError::UnexpectedEnd);
1434 }
1435 self.0.extend_from_slice(bytes);
1437 Ok(())
1438 }
1439}
1440
1441impl KeyFramesManager {
1442 fn distributed(enabled: bool, keyframe_interval: u32) -> CuResult<Self> {
1443 if enabled && keyframe_interval == 0 {
1444 return Err(CuError::from("Keyframe interval cannot be zero"));
1445 }
1446 let mut manager = Self::new(None, keyframe_interval)?;
1447 manager.distributed = enabled;
1448 Ok(manager)
1449 }
1450
1451 #[doc(hidden)]
1452 pub fn capture_interval(&self) -> u32 {
1453 self.keyframe_interval
1454 }
1455
1456 #[doc(hidden)]
1457 pub fn new(sink: Option<Box<CompletedKeyFrameSink>>, keyframe_interval: u32) -> CuResult<Self> {
1458 if sink.is_some() && keyframe_interval == 0 {
1459 return Err(CuError::from(
1460 "Keyframe interval cannot be zero when a downstream consumer requires keyframes",
1461 ));
1462 }
1463
1464 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1465 {
1466 let enabled = sink.is_some();
1467 let (
1468 pending_producer,
1469 completion_consumer,
1470 worker_handle,
1471 worker_thread,
1472 worker_shutdown,
1473 worker_running,
1474 ) = if let Some(mut sink) = sink {
1475 let (pending_producer, mut pending_consumer) =
1476 RingBuffer::<Box<KeyFrame>>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1477 let (mut completion_producer, completion_consumer) =
1478 RingBuffer::<AsyncKeyFrameCompletion>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1479 let worker_shutdown = Arc::new(AtomicBool::new(false));
1480 let worker_running = Arc::new(AtomicBool::new(true));
1481 let shutdown = worker_shutdown.clone();
1482 let running = worker_running.clone();
1483 let worker_handle = std::thread::Builder::new()
1484 .name("cu-async-kf-io".to_string())
1485 .spawn(move || {
1486 let _running_guard = AsyncOutputWorkerRunningGuard(running);
1487 loop {
1488 let keyframe = match pending_consumer.pop() {
1489 Ok(keyframe) => keyframe,
1490 Err(PopError::Empty) => {
1491 if shutdown.load(Ordering::Acquire) {
1492 break;
1493 }
1494 std::thread::park();
1495 continue;
1496 }
1497 };
1498 let sink_result = sink
1499 .log(keyframe.as_ref())
1500 .map(|_| sink.last_log_bytes().unwrap_or(0) as u64);
1501 let should_stop = sink_result.is_err();
1502 let mut completion = AsyncKeyFrameCompletion {
1503 keyframe,
1504 sink_result,
1505 };
1506 loop {
1507 match completion_producer.push(completion) {
1508 Ok(()) => break,
1509 Err(PushError::Full(returned)) => {
1510 completion = returned;
1511 std::thread::yield_now();
1512 }
1513 }
1514 }
1515 if should_stop {
1516 break;
1517 }
1518 }
1519 })
1520 .map_err(|error| {
1521 CuError::from("Failed to spawn async keyframe output thread")
1522 .add_cause(error.to_string().as_str())
1523 })?;
1524 let worker_thread = worker_handle.thread().clone();
1525 (
1526 Some(pending_producer),
1527 Some(completion_consumer),
1528 Some(worker_handle),
1529 Some(worker_thread),
1530 Some(worker_shutdown),
1531 Some(worker_running),
1532 )
1533 } else {
1534 (None, None, None, None, None, None)
1535 };
1536
1537 let spares = if enabled {
1538 let mut spares = Vec::with_capacity(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1539 for _ in 0..ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1540 spares.push(Box::new(KeyFrame::new()));
1541 }
1542 spares
1543 } else {
1544 Vec::new()
1545 };
1546 Ok(Self {
1547 distributed: false,
1548 capture_capacities: Vec::new(),
1549 inner: enabled.then(KeyFrame::new),
1550 forced_timestamp: None,
1551 locked: false,
1552 spares,
1553 pending_count: 0,
1554 pending_producer,
1555 completion_consumer,
1556 worker_handle,
1557 worker_thread,
1558 worker_shutdown,
1559 worker_running,
1560 capture_this_copperlist: None,
1561 dropped_keyframes_total: 0,
1562 keyframe_interval,
1563 last_encoded_bytes: 0,
1564 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1565 })
1566 }
1567
1568 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1569 {
1570 let enabled = sink.is_some();
1571 Ok(Self {
1572 distributed: false,
1573 capture_capacities: Vec::new(),
1574 inner: enabled.then(KeyFrame::new),
1575 forced_timestamp: None,
1576 locked: false,
1577 sink,
1578 keyframe_interval,
1579 last_encoded_bytes: 0,
1580 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1581 })
1582 }
1583 }
1584
1585 fn is_keyframe_due(&self, culistid: u64) -> bool {
1586 (self.inner.is_some() || self.distributed)
1587 && culistid.is_multiple_of(self.keyframe_interval as u64)
1588 }
1589
1590 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1591 fn is_capturing(&self, culistid: u64) -> bool {
1592 self.capture_this_copperlist == Some(culistid)
1593 }
1594
1595 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1596 fn is_capturing(&self, culistid: u64) -> bool {
1597 self.is_keyframe_due(culistid)
1598 }
1599
1600 #[inline]
1601 pub fn captures_keyframe(&self, culistid: u64) -> bool {
1602 self.is_keyframe_due(culistid)
1603 }
1604
1605 #[doc(hidden)]
1607 pub fn begin_capture_preallocation(&mut self) {
1608 self.capture_size_hint = KEYFRAME_PAYLOAD_HEADER.len();
1609 self.capture_capacities.clear();
1610 }
1611
1612 #[doc(hidden)]
1614 pub fn include_capture_capacity(&mut self, item: &impl Freezable) -> CuResult<()> {
1615 if self.inner.is_none() && !self.distributed {
1616 return Ok(());
1617 }
1618 let mut sizer = EncoderImpl::new(SizeWriter::default(), bincode::config::standard());
1619 BincodeAdapter(item)
1620 .encode(&mut sizer)
1621 .map_err(|_| CuError::from("Failed to size component keyframe state"))?;
1622 let payload_bytes = sizer.into_writer().bytes_written as usize;
1623 if self.distributed {
1624 self.capture_capacities.push(
1625 (KEYFRAME_FRAME_HEADER_LEN + payload_bytes)
1626 .max(DISTRIBUTED_REGION_ALIGN)
1627 .checked_next_power_of_two()
1628 .ok_or_else(|| CuError::from("Keyframe region size overflow"))?,
1629 );
1630 }
1631 self.capture_size_hint = self
1632 .capture_size_hint
1633 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
1634 .and_then(|size| size.checked_add(payload_bytes))
1635 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1636 Ok(())
1637 }
1638
1639 #[doc(hidden)]
1641 pub fn finish_capture_preallocation(&mut self) -> CuResult<()> {
1642 if self.distributed {
1643 return Ok(());
1644 }
1645 if self.inner.is_none() {
1646 return Ok(());
1647 }
1648 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1649 self.reclaim_completed()?;
1650 let requested = self
1651 .capture_size_hint
1652 .max(MIN_KEYFRAME_CAPTURE_CAPACITY)
1653 .checked_next_power_of_two()
1654 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1655 reserve_keyframe_capacity(self.inner.as_mut().unwrap(), requested)?;
1656 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1657 for spare in &mut self.spares {
1658 reserve_keyframe_capacity(spare, requested)?;
1659 }
1660 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1661 if self.spares.len() != ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1662 return Err(CuError::from(
1663 "Keyframe output worker did not return every capture buffer before preallocation",
1664 ));
1665 }
1666 Ok(())
1667 }
1668
1669 #[doc(hidden)]
1672 pub fn try_reset(&mut self, culistid: u64, clock: &RobotClock) -> CuResult<()> {
1673 if self.is_keyframe_due(culistid) {
1674 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1675 {
1676 self.reclaim_completed()?;
1677 if self.spares.is_empty() {
1678 self.capture_this_copperlist = None;
1679 self.dropped_keyframes_total = self.dropped_keyframes_total.saturating_add(1);
1680 self.forced_timestamp = None;
1681 self.locked = false;
1682 return Ok(());
1683 }
1684 self.capture_this_copperlist = Some(culistid);
1685 }
1686 let inner = self.inner.as_mut().unwrap();
1688 if self.locked && inner.culistid == culistid {
1689 return Ok(());
1690 }
1691 let ts = self.forced_timestamp.take().unwrap_or_else(|| clock.now());
1692 inner.reset(culistid, ts);
1693 self.locked = false;
1694 }
1695 Ok(())
1696 }
1697
1698 pub fn reset(&mut self, culistid: u64, clock: &RobotClock) {
1703 let _ = self.try_reset(culistid, clock);
1704 }
1705
1706 #[cfg(feature = "std")]
1708 pub fn set_forced_timestamp(&mut self, ts: CuTime) {
1709 self.forced_timestamp = Some(ts);
1710 }
1711
1712 pub fn freeze_task(&mut self, culistid: u64, task: &impl Freezable) -> CuResult<usize> {
1713 if self.is_capturing(culistid) {
1714 if self.locked {
1715 return Ok(0);
1717 }
1718 let inner = self.inner.as_mut().unwrap();
1719 if inner.culistid != culistid {
1720 return Err(CuError::from(format!(
1721 "Freezing task for culistid {} but current keyframe is {}",
1722 culistid, inner.culistid
1723 )));
1724 }
1725 let encoded = inner
1726 .add_frozen_task(task)
1727 .map_err(|e| CuError::from(format!("Failed to serialize task: {e}")))?;
1728 Ok(encoded)
1729 } else {
1730 Ok(0)
1731 }
1732 }
1733
1734 pub fn freeze_any(&mut self, culistid: u64, item: &impl Freezable) -> CuResult<usize> {
1736 self.freeze_task(culistid, item)
1737 }
1738
1739 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1740 if self.is_capturing(culistid) {
1741 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1742 {
1743 let sink = self.sink.as_mut().unwrap();
1744 sink.log(self.inner.as_ref().unwrap())?;
1745 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
1746 }
1747 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1748 {
1749 self.last_encoded_bytes = 0;
1750 let mut completed = self.spares.pop().ok_or_else(|| {
1751 CuError::from("Missing spare keyframe buffer at output handoff")
1752 })?;
1753 core::mem::swap(self.inner.as_mut().unwrap(), completed.as_mut());
1754 let producer = self.pending_producer.as_mut().ok_or_else(|| {
1755 CuError::from("Missing keyframe output producer for active capture")
1756 })?;
1757 match producer.push(completed) {
1758 Ok(()) => {
1759 self.pending_count += 1;
1760 if let Some(worker_thread) = self.worker_thread.as_ref() {
1761 worker_thread.unpark();
1762 }
1763 }
1764 Err(PushError::Full(spare)) => {
1765 self.spares.push(spare);
1766 self.dropped_keyframes_total =
1767 self.dropped_keyframes_total.saturating_add(1);
1768 }
1769 }
1770 self.capture_this_copperlist = None;
1771 }
1772 self.locked = false;
1774 Ok(())
1775 } else {
1776 self.last_encoded_bytes = 0;
1778 Ok(())
1779 }
1780 }
1781
1782 #[cfg(feature = "std")]
1784 pub fn lock_keyframe(&mut self, keyframe: &KeyFrame) {
1785 if let Some(inner) = self.inner.as_mut() {
1786 *inner = keyframe.clone();
1787 self.forced_timestamp = Some(keyframe.timestamp);
1788 self.locked = true;
1789 }
1790 }
1791
1792 #[inline]
1793 #[doc(hidden)]
1794 pub const fn dropped_keyframes_total(&self) -> u64 {
1795 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1796 {
1797 self.dropped_keyframes_total
1798 }
1799 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1800 {
1801 0
1802 }
1803 }
1804
1805 #[doc(hidden)]
1806 pub fn close_output(&mut self) -> CuResult<()> {
1807 self.finish_pending()?;
1808 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1809 self.shutdown_worker()?;
1810 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1811 {
1812 if let Some(sink) = &mut self.sink {
1813 sink.flush()?;
1814 }
1815 self.sink.take();
1816 }
1817 Ok(())
1818 }
1819
1820 #[doc(hidden)]
1821 pub fn finish_pending(&mut self) -> CuResult<()> {
1822 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1823 while self.pending_count > 0 {
1824 self.wait_for_completion()?;
1825 }
1826 Ok(())
1827 }
1828
1829 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1830 fn reclaim_completed(&mut self) -> CuResult<()> {
1831 let pop_result = {
1832 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1833 return Ok(());
1834 };
1835 completion_consumer.pop()
1836 };
1837 match pop_result {
1838 Ok(completion) => self.handle_completion(completion),
1839 Err(PopError::Empty) => {
1840 if self.pending_count > 0
1841 && self
1842 .worker_running
1843 .as_ref()
1844 .is_some_and(|running| !running.load(Ordering::Acquire))
1845 {
1846 Err(CuError::from(
1847 "Async keyframe output worker stopped unexpectedly",
1848 ))
1849 } else {
1850 Ok(())
1851 }
1852 }
1853 }
1854 }
1855
1856 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1857 fn wait_for_completion(&mut self) -> CuResult<()> {
1858 loop {
1859 let pending_before = self.pending_count;
1860 self.reclaim_completed()?;
1861 if self.pending_count < pending_before {
1862 return Ok(());
1863 }
1864 std::thread::yield_now();
1865 }
1866 }
1867
1868 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1869 fn handle_completion(&mut self, completion: AsyncKeyFrameCompletion) -> CuResult<()> {
1870 self.pending_count = self.pending_count.saturating_sub(1);
1871 if let Ok(encoded_bytes) = completion.sink_result.as_ref() {
1872 self.last_encoded_bytes = *encoded_bytes;
1873 }
1874 self.spares.push(completion.keyframe);
1875 completion.sink_result.map(|_| ())
1876 }
1877
1878 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1879 fn shutdown_worker(&mut self) -> CuResult<()> {
1880 self.finish_pending()?;
1881 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1882 shutdown.store(true, Ordering::Release);
1883 }
1884 if let Some(worker_thread) = self.worker_thread.as_ref() {
1885 worker_thread.unpark();
1886 }
1887 if let Some(worker_handle) = self.worker_handle.take() {
1888 worker_handle.join().map_err(|_| {
1889 CuError::from("Async keyframe output worker panicked while joining")
1890 })?;
1891 }
1892 self.pending_producer.take();
1893 self.worker_thread.take();
1894 self.worker_shutdown.take();
1895 self.worker_running.take();
1896 Ok(())
1897 }
1898}
1899
1900fn reserve_keyframe_capacity(keyframe: &mut KeyFrame, requested: usize) -> CuResult<()> {
1901 if keyframe.serialized_tasks.capacity() < requested {
1902 let additional = requested.saturating_sub(keyframe.serialized_tasks.len());
1903 keyframe
1904 .serialized_tasks
1905 .try_reserve_exact(additional)
1906 .map_err(|error| {
1907 CuError::from("Failed to preallocate keyframe capture buffer")
1908 .add_cause(&error.to_string())
1909 })?;
1910 }
1911 Ok(())
1912}
1913
1914#[cfg(all(feature = "std", feature = "async-cl-io"))]
1915impl Drop for KeyFramesManager {
1916 fn drop(&mut self) {
1917 let _ = self.shutdown_worker();
1918 }
1919}
1920
1921pub struct CuRuntime<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> {
1925 clock: RobotClock,
1927
1928 subsystem_code: u16,
1930
1931 #[doc(hidden)]
1933 pub instance_id: u32,
1934
1935 #[doc(hidden)]
1937 pub tasks: CT,
1938
1939 #[doc(hidden)]
1941 pub bridges: CB,
1942
1943 #[doc(hidden)]
1945 pub resources: ResourceManager,
1946
1947 #[cfg(feature = "std")]
1950 #[doc(hidden)]
1951 pub thread_pools: Vec<Option<Arc<ThreadPool>>>,
1952
1953 #[doc(hidden)]
1955 pub monitor: M,
1956
1957 #[cfg(feature = "std")]
1963 #[doc(hidden)]
1964 pub execution_probe: ExecutionProbeHandle,
1965 #[cfg(not(feature = "std"))]
1966 #[doc(hidden)]
1967 pub execution_probe: RuntimeExecutionProbe,
1968
1969 #[doc(hidden)]
1971 pub copperlists_manager: CopperListsManager<P, NBCL>,
1972
1973 #[doc(hidden)]
1975 pub keyframes_manager: KeyFramesManager,
1976
1977 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1979 #[doc(hidden)]
1980 pub parallel_rt: ParallelRt<NBCL>,
1981
1982 #[doc(hidden)]
1984 pub runtime_config: RuntimeConfig,
1985}
1986
1987impl<
1989 CT,
1990 CB,
1991 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload,
1992 M: CuMonitor,
1993 const NBCL: usize,
1994> ClockProvider for CuRuntime<CT, CB, P, M, NBCL>
1995{
1996 fn get_clock(&self) -> RobotClock {
1997 self.clock.clone()
1998 }
1999}
2000
2001impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> CuRuntime<CT, CB, P, M, NBCL> {
2002 #[inline]
2004 pub fn clock(&self) -> RobotClock {
2005 self.clock.clone()
2006 }
2007
2008 #[doc(hidden)]
2010 #[inline]
2011 pub fn clock_ref(&self) -> &RobotClock {
2012 &self.clock
2013 }
2014
2015 #[inline]
2017 pub fn subsystem_code(&self) -> u16 {
2018 self.subsystem_code
2019 }
2020
2021 #[inline]
2023 pub fn instance_id(&self) -> u32 {
2024 self.instance_id
2025 }
2026}
2027
2028#[cfg(feature = "std")]
2029impl<
2030 'cfg,
2031 CT,
2032 CB,
2033 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2034 M: CuMonitor,
2035 const NBCL: usize,
2036 TI,
2037 BI,
2038 MI,
2039 CLS,
2040 KFS,
2041> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
2042where
2043 TI: for<'c> Fn(
2044 Vec<Option<&'c ComponentConfig>>,
2045 &mut ResourceManager,
2046 &[Option<Arc<ThreadPool>>],
2047 ) -> CuResult<CT>,
2048 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
2049 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
2050 CLS: WriteStream<CopperList<P>> + 'static,
2051 KFS: WriteStream<KeyFrame> + 'static,
2052{
2053 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
2054 let Self {
2055 clock,
2056 config,
2057 mission,
2058 subsystem,
2059 instance_id,
2060 resources,
2061 thread_pools,
2062 parts,
2063 copperlist_sink,
2064 keyframe_sink,
2065 output_requirements,
2066 } = self;
2067 let mut resources =
2068 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
2069 let thread_pools = thread_pools.unwrap_or_default();
2070
2071 let graph = config.get_graph(Some(mission))?;
2072 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
2073 .get_all_nodes()
2074 .iter()
2075 .map(|(_, node)| node.get_instance_config())
2076 .collect();
2077
2078 let tasks =
2079 (parts.tasks_instanciator)(all_instances_configs, &mut resources, &thread_pools)?;
2080
2081 #[cfg(feature = "std")]
2082 let execution_probe = std::sync::Arc::new(RuntimeExecutionProbe::default());
2083 #[cfg(not(feature = "std"))]
2084 let execution_probe = RuntimeExecutionProbe::default();
2085 let monitor_metadata = CuMonitoringMetadata::new(
2086 CompactString::from(mission),
2087 parts.monitored_components,
2088 parts.culist_component_mapping,
2089 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
2090 build_monitor_topology(config, mission)?,
2091 None,
2092 )?
2093 .with_subsystem_id(subsystem.id())
2094 .with_instance_id(instance_id);
2095 #[cfg(feature = "std")]
2096 let monitor_runtime =
2097 CuMonitoringRuntime::new(MonitorExecutionProbe::from_shared(execution_probe.clone()));
2098 #[cfg(not(feature = "std"))]
2099 let monitor_runtime = CuMonitoringRuntime::unavailable();
2100 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
2101 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
2102
2103 let copperlist_sink = output_requirements
2104 .completed_copperlists()
2105 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
2106 let keyframe_sink = output_requirements
2107 .keyframes()
2108 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
2109 let keyframe_interval = config
2110 .logging
2111 .as_ref()
2112 .and_then(|logging| logging.keyframe_interval)
2113 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
2114
2115 let (copperlists_manager, keyframes_manager) = if P::DISTRIBUTED {
2116 let keyframes_manager =
2117 KeyFramesManager::distributed(keyframe_sink.is_some(), keyframe_interval)?;
2118 (
2119 CopperListsManager::new_with_keyframes(copperlist_sink, keyframe_sink)?,
2120 keyframes_manager,
2121 )
2122 } else {
2123 (
2124 CopperListsManager::new(copperlist_sink)?,
2125 KeyFramesManager::new(keyframe_sink, keyframe_interval)?,
2126 )
2127 };
2128 #[cfg(target_os = "none")]
2129 {
2130 let cl_size = core::mem::size_of::<CopperList<P>>();
2131 let total_bytes = cl_size.saturating_mul(NBCL);
2132 info!(
2133 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
2134 NBCL, cl_size, total_bytes
2135 );
2136 }
2137
2138 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2139 let parallel_rt = ParallelRt::new(parts.parallel_rt_metadata)?;
2140
2141 let runtime_config = config.runtime.clone().unwrap_or_default();
2142 runtime_config.validate()?;
2143
2144 Ok(CuRuntime {
2145 subsystem_code: subsystem.code(),
2146 instance_id,
2147 tasks,
2148 bridges,
2149 resources,
2150 thread_pools,
2151 monitor,
2152 execution_probe,
2153 clock,
2154 copperlists_manager,
2155 keyframes_manager,
2156 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2157 parallel_rt,
2158 runtime_config,
2159 })
2160 }
2161}
2162
2163#[cfg(not(feature = "std"))]
2164impl<
2165 'cfg,
2166 CT,
2167 CB,
2168 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2169 M: CuMonitor,
2170 const NBCL: usize,
2171 TI,
2172 BI,
2173 MI,
2174 CLS,
2175 KFS,
2176> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
2177where
2178 TI: for<'c> Fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>,
2179 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
2180 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
2181 CLS: WriteStream<CopperList<P>> + 'static,
2182 KFS: WriteStream<KeyFrame> + 'static,
2183{
2184 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
2185 let Self {
2186 clock,
2187 config,
2188 mission,
2189 subsystem,
2190 instance_id,
2191 resources,
2192 parts,
2193 copperlist_sink,
2194 keyframe_sink,
2195 output_requirements,
2196 } = self;
2197 let mut resources =
2198 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
2199
2200 let graph = config.get_graph(Some(mission))?;
2201 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
2202 .get_all_nodes()
2203 .iter()
2204 .map(|(_, node)| node.get_instance_config())
2205 .collect();
2206
2207 let tasks = (parts.tasks_instanciator)(all_instances_configs, &mut resources)?;
2208
2209 let execution_probe = RuntimeExecutionProbe::default();
2210 let monitor_metadata = CuMonitoringMetadata::new(
2211 CompactString::from(mission),
2212 parts.monitored_components,
2213 parts.culist_component_mapping,
2214 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
2215 build_monitor_topology(config, mission)?,
2216 None,
2217 )?
2218 .with_subsystem_id(subsystem.id())
2219 .with_instance_id(instance_id);
2220 let monitor_runtime = CuMonitoringRuntime::unavailable();
2221 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
2222 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
2223
2224 let copperlist_sink = output_requirements
2225 .completed_copperlists()
2226 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
2227 let keyframe_sink = output_requirements
2228 .keyframes()
2229 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
2230 let keyframe_interval = config
2231 .logging
2232 .as_ref()
2233 .and_then(|logging| logging.keyframe_interval)
2234 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
2235
2236 let (copperlists_manager, keyframes_manager) = if P::DISTRIBUTED {
2237 let keyframes_manager =
2238 KeyFramesManager::distributed(keyframe_sink.is_some(), keyframe_interval)?;
2239 (
2240 CopperListsManager::new_with_keyframes(copperlist_sink, keyframe_sink)?,
2241 keyframes_manager,
2242 )
2243 } else {
2244 (
2245 CopperListsManager::new(copperlist_sink)?,
2246 KeyFramesManager::new(keyframe_sink, keyframe_interval)?,
2247 )
2248 };
2249 #[cfg(target_os = "none")]
2250 {
2251 let cl_size = core::mem::size_of::<CopperList<P>>();
2252 let total_bytes = cl_size.saturating_mul(NBCL);
2253 info!(
2254 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
2255 NBCL, cl_size, total_bytes
2256 );
2257 }
2258
2259 let runtime_config = config.runtime.clone().unwrap_or_default();
2260 runtime_config.validate()?;
2261
2262 Ok(CuRuntime {
2263 subsystem_code: subsystem.code(),
2264 instance_id,
2265 tasks,
2266 bridges,
2267 resources,
2268 monitor,
2269 execution_probe,
2270 clock,
2271 copperlists_manager,
2272 keyframes_manager,
2273 runtime_config,
2274 })
2275 }
2276}
2277
2278#[derive(Clone, Decode)]
2283pub struct KeyFrame {
2284 pub culistid: u64,
2286 pub timestamp: CuTime,
2288 pub serialized_tasks: Vec<u8>,
2290}
2291
2292const DISTRIBUTED_KEYFRAME_HEADER: &[u8; 5] = b"CUKD\x01";
2293const DISTRIBUTED_ACTIVE_OFFSET: usize = DISTRIBUTED_KEYFRAME_HEADER.len();
2294const DISTRIBUTED_COUNT_OFFSET: usize = DISTRIBUTED_ACTIVE_OFFSET + 1;
2295const DISTRIBUTED_TABLE_OFFSET: usize = DISTRIBUTED_COUNT_OFFSET + 4;
2296const DISTRIBUTED_ENTRY_LEN: usize = 16;
2297const DISTRIBUTED_REGION_LEN: usize = 8;
2298const DISTRIBUTED_REGION_ALIGN: usize = 128;
2299
2300struct RegionWriter<'a> {
2301 bytes: &'a mut [u8],
2302 written: usize,
2303}
2304
2305impl Writer for RegionWriter<'_> {
2306 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
2307 let end = self
2308 .written
2309 .checked_add(bytes.len())
2310 .ok_or(EncodeError::UnexpectedEnd)?;
2311 self.bytes
2312 .get_mut(self.written..end)
2313 .ok_or(EncodeError::UnexpectedEnd)?
2314 .copy_from_slice(bytes);
2315 self.written = end;
2316 Ok(())
2317 }
2318}
2319
2320fn distributed_count(bytes: &[u8]) -> Result<Option<usize>, &'static str> {
2321 if !bytes.starts_with(DISTRIBUTED_KEYFRAME_HEADER) {
2322 return Ok(None);
2323 }
2324 let count_end = DISTRIBUTED_COUNT_OFFSET + 4;
2325 let count = u32::from_le_bytes(
2326 bytes
2327 .get(DISTRIBUTED_COUNT_OFFSET..count_end)
2328 .ok_or("distributed keyframe header is truncated")?
2329 .try_into()
2330 .map_err(|_| "distributed keyframe count is invalid")?,
2331 ) as usize;
2332 let table_end = DISTRIBUTED_TABLE_OFFSET
2333 .checked_add(
2334 count
2335 .checked_mul(DISTRIBUTED_ENTRY_LEN)
2336 .ok_or("distributed keyframe table overflows")?,
2337 )
2338 .ok_or("distributed keyframe table overflows")?;
2339 if table_end > bytes.len() {
2340 return Err("distributed keyframe table is truncated");
2341 }
2342 Ok(Some(count))
2343}
2344
2345fn distributed_region(bytes: &[u8], component: usize) -> Result<(usize, usize), &'static str> {
2346 let count = distributed_count(bytes)?.ok_or("distributed keyframe storage is unavailable")?;
2347 if component >= count {
2348 return Err("keyframe component is out of bounds");
2349 }
2350 let entry = DISTRIBUTED_TABLE_OFFSET + component * DISTRIBUTED_ENTRY_LEN;
2351 let offset = u64::from_le_bytes(
2352 bytes[entry..entry + 8]
2353 .try_into()
2354 .map_err(|_| "distributed keyframe offset is invalid")?,
2355 );
2356 let capacity = u64::from_le_bytes(
2357 bytes[entry + 8..entry + DISTRIBUTED_ENTRY_LEN]
2358 .try_into()
2359 .map_err(|_| "distributed keyframe capacity is invalid")?,
2360 );
2361 let offset = usize::try_from(offset).map_err(|_| "distributed keyframe offset is too large")?;
2362 let capacity =
2363 usize::try_from(capacity).map_err(|_| "distributed keyframe capacity is too large")?;
2364 let end = offset
2365 .checked_add(DISTRIBUTED_REGION_LEN)
2366 .and_then(|value| value.checked_add(capacity))
2367 .ok_or("distributed keyframe region overflows")?;
2368 if end > bytes.len() {
2369 return Err("distributed keyframe region is truncated");
2370 }
2371 Ok((offset, capacity))
2372}
2373
2374unsafe fn distributed_region_raw(
2375 ptr: *const u8,
2376 allocation_len: usize,
2377 component: usize,
2378) -> Result<(usize, usize), &'static str> {
2379 if allocation_len < DISTRIBUTED_COUNT_OFFSET + 4 {
2380 return Err("distributed keyframe header is truncated");
2381 }
2382 for (index, expected) in DISTRIBUTED_KEYFRAME_HEADER.iter().copied().enumerate() {
2383 if unsafe { ptr.add(index).read() } != expected {
2386 return Err("distributed keyframe storage is unavailable");
2387 }
2388 }
2389 let count = u32::from_le(unsafe {
2392 ptr.add(DISTRIBUTED_COUNT_OFFSET)
2393 .cast::<u32>()
2394 .read_unaligned()
2395 }) as usize;
2396 let table_end = DISTRIBUTED_TABLE_OFFSET
2397 .checked_add(
2398 count
2399 .checked_mul(DISTRIBUTED_ENTRY_LEN)
2400 .ok_or("distributed keyframe table overflows")?,
2401 )
2402 .ok_or("distributed keyframe table overflows")?;
2403 if table_end > allocation_len {
2404 return Err("distributed keyframe table is truncated");
2405 }
2406 if component >= count {
2407 return Err("keyframe component is out of bounds");
2408 }
2409 let entry = DISTRIBUTED_TABLE_OFFSET + component * DISTRIBUTED_ENTRY_LEN;
2410 let offset = u64::from_le(unsafe { ptr.add(entry).cast::<u64>().read_unaligned() });
2412 let capacity = u64::from_le(unsafe { ptr.add(entry + 8).cast::<u64>().read_unaligned() });
2414 let offset = usize::try_from(offset).map_err(|_| "distributed keyframe offset is too large")?;
2415 let capacity =
2416 usize::try_from(capacity).map_err(|_| "distributed keyframe capacity is too large")?;
2417 let end = offset
2418 .checked_add(DISTRIBUTED_REGION_LEN)
2419 .and_then(|value| value.checked_add(capacity))
2420 .ok_or("distributed keyframe region overflows")?;
2421 if end > allocation_len {
2422 return Err("distributed keyframe region is truncated");
2423 }
2424 Ok((offset, capacity))
2425}
2426
2427fn distributed_frame(bytes: &[u8], component: usize) -> Result<&[u8], &'static str> {
2428 let (offset, capacity) = distributed_region(bytes, component)?;
2429 let frame_len = u64::from_le_bytes(
2430 bytes[offset..offset + DISTRIBUTED_REGION_LEN]
2431 .try_into()
2432 .map_err(|_| "distributed keyframe length is invalid")?,
2433 );
2434 let frame_len =
2435 usize::try_from(frame_len).map_err(|_| "distributed keyframe length is too large")?;
2436 if !(KEYFRAME_FRAME_HEADER_LEN..=capacity).contains(&frame_len) {
2437 return Err("distributed keyframe component is incomplete");
2438 }
2439 let frame_start = offset + DISTRIBUTED_REGION_LEN;
2440 Ok(&bytes[frame_start..frame_start + frame_len])
2441}
2442
2443fn align_up(value: usize, alignment: usize) -> Option<usize> {
2444 value
2445 .checked_add(alignment - 1)
2446 .map(|value| value & !(alignment - 1))
2447}
2448
2449impl Encode for KeyFrame {
2450 fn encode<__E: bincode::enc::Encoder>(&self, encoder: &mut __E) -> Result<(), EncodeError> {
2451 self.culistid.encode(encoder)?;
2452 self.timestamp.encode(encoder)?;
2453 let bytes = self.serialized_tasks.as_slice();
2454 let Some(count) = distributed_count(bytes).map_err(EncodeError::Other)? else {
2455 return self.serialized_tasks.encode(encoder);
2456 };
2457 let mut length = KEYFRAME_PAYLOAD_HEADER.len();
2458 for component in 0..count {
2459 length = length
2460 .checked_add(
2461 distributed_frame(bytes, component)
2462 .map_err(EncodeError::Other)?
2463 .len(),
2464 )
2465 .ok_or(EncodeError::Other("distributed keyframe length overflows"))?;
2466 }
2467 length.encode(encoder)?;
2468 encoder.writer().write(KEYFRAME_PAYLOAD_HEADER)?;
2469 for component in 0..count {
2470 encoder
2471 .writer()
2472 .write(distributed_frame(bytes, component).map_err(EncodeError::Other)?)?;
2473 }
2474 Ok(())
2475 }
2476}
2477
2478impl KeyFrame {
2479 pub(crate) fn from_serialized(
2480 culistid: u64,
2481 timestamp: CuTime,
2482 serialized_tasks: Vec<u8>,
2483 ) -> Self {
2484 Self {
2485 culistid,
2486 timestamp,
2487 serialized_tasks,
2488 }
2489 }
2490
2491 #[inline]
2492 #[cfg(feature = "std")]
2493 pub(crate) fn is_active(&self) -> bool {
2494 self.serialized_tasks
2495 .starts_with(DISTRIBUTED_KEYFRAME_HEADER)
2496 && self.serialized_tasks[DISTRIBUTED_ACTIVE_OFFSET] != 0
2497 }
2498
2499 #[doc(hidden)]
2501 pub fn distributed(capacities: &[usize]) -> Self {
2502 let table_end = DISTRIBUTED_TABLE_OFFSET
2503 .checked_add(
2504 capacities
2505 .len()
2506 .checked_mul(DISTRIBUTED_ENTRY_LEN)
2507 .expect("distributed keyframe region table overflow"),
2508 )
2509 .expect("distributed keyframe region table overflow");
2510 let slab_bytes = capacities.iter().fold(0usize, |total, &capacity| {
2511 let capacity = capacity.max(KEYFRAME_FRAME_HEADER_LEN);
2512 let region_bytes =
2513 align_up(DISTRIBUTED_REGION_LEN + capacity, DISTRIBUTED_REGION_ALIGN)
2514 .expect("distributed keyframe region size overflow");
2515 total
2516 .checked_add(region_bytes)
2517 .expect("distributed keyframe slab size overflow")
2518 });
2519 let maximum = table_end
2520 .checked_add(DISTRIBUTED_REGION_ALIGN - 1)
2521 .and_then(|value| value.checked_add(slab_bytes))
2522 .expect("distributed keyframe allocation size overflow");
2523 let mut serialized_tasks = alloc::vec![0; maximum];
2524 serialized_tasks[..DISTRIBUTED_KEYFRAME_HEADER.len()]
2525 .copy_from_slice(DISTRIBUTED_KEYFRAME_HEADER);
2526 let count = u32::try_from(capacities.len()).expect("too many keyframe components");
2527 serialized_tasks[DISTRIBUTED_COUNT_OFFSET..DISTRIBUTED_COUNT_OFFSET + 4]
2528 .copy_from_slice(&count.to_le_bytes());
2529
2530 let allocation = serialized_tasks.as_ptr() as usize;
2531 let mut region_offset = align_up(
2532 allocation
2533 .checked_add(table_end)
2534 .expect("distributed keyframe address overflow"),
2535 DISTRIBUTED_REGION_ALIGN,
2536 )
2537 .expect("distributed keyframe alignment overflow")
2538 - allocation;
2539 for (component, &capacity) in capacities.iter().enumerate() {
2540 let capacity = capacity.max(KEYFRAME_FRAME_HEADER_LEN);
2541 let entry = DISTRIBUTED_TABLE_OFFSET + component * DISTRIBUTED_ENTRY_LEN;
2542 serialized_tasks[entry..entry + 8]
2543 .copy_from_slice(&(region_offset as u64).to_le_bytes());
2544 serialized_tasks[entry + 8..entry + DISTRIBUTED_ENTRY_LEN]
2545 .copy_from_slice(&(capacity as u64).to_le_bytes());
2546 region_offset += align_up(DISTRIBUTED_REGION_LEN + capacity, DISTRIBUTED_REGION_ALIGN)
2547 .expect("distributed keyframe region size overflow");
2548 }
2549 serialized_tasks.truncate(region_offset);
2550 Self {
2551 culistid: 0,
2552 timestamp: CuTime::default(),
2553 serialized_tasks,
2554 }
2555 }
2556
2557 #[doc(hidden)]
2558 pub fn reset_distributed(&mut self, culistid: u64, timestamp: CuTime, interval: u32) {
2559 self.culistid = culistid;
2560 self.timestamp = timestamp;
2561 let active = interval != 0 && culistid.is_multiple_of(u64::from(interval));
2562 if distributed_count(&self.serialized_tasks)
2563 .ok()
2564 .flatten()
2565 .is_some()
2566 {
2567 self.serialized_tasks[DISTRIBUTED_ACTIVE_OFFSET] = u8::from(active);
2568 }
2569 if active {
2570 let count = distributed_count(&self.serialized_tasks)
2571 .expect("distributed keyframe descriptor")
2572 .expect("distributed keyframe storage");
2573 for component in 0..count {
2574 let (offset, _) = distributed_region(&self.serialized_tasks, component)
2575 .expect("distributed keyframe region");
2576 self.serialized_tasks[offset..offset + DISTRIBUTED_REGION_LEN].fill(0);
2577 }
2578 }
2579 }
2580
2581 #[doc(hidden)]
2582 #[inline]
2583 pub fn discard_distributed(&mut self) {
2584 if distributed_count(&self.serialized_tasks)
2585 .ok()
2586 .flatten()
2587 .is_some()
2588 {
2589 self.serialized_tasks[DISTRIBUTED_ACTIVE_OFFSET] = 0;
2590 }
2591 }
2592
2593 #[doc(hidden)]
2601 pub unsafe fn freeze_region(
2602 ptr: *mut u8,
2603 allocation_len: usize,
2604 component: usize,
2605 task: &impl Freezable,
2606 ) -> CuResult<usize> {
2607 if ptr.is_null() || allocation_len <= DISTRIBUTED_ACTIVE_OFFSET {
2608 return Ok(0);
2609 }
2610 if unsafe { ptr.add(DISTRIBUTED_ACTIVE_OFFSET).read() } == 0 {
2611 return Ok(0);
2612 }
2613 let (offset, capacity) =
2616 unsafe { distributed_region_raw(ptr.cast_const(), allocation_len, component) }
2617 .map_err(CuError::from)?;
2618 let length = u64::from_le(unsafe { ptr.add(offset).cast::<u64>().read_unaligned() });
2621 if length != 0 {
2622 return Err(CuError::from(
2623 "Component captured twice in one distributed keyframe",
2624 ));
2625 }
2626 let frame = unsafe {
2629 core::slice::from_raw_parts_mut(ptr.add(offset + DISTRIBUTED_REGION_LEN), capacity)
2630 };
2631 let mut writer = RegionWriter {
2632 bytes: &mut frame[KEYFRAME_FRAME_HEADER_LEN..],
2633 written: 0,
2634 };
2635 let mut encoder = EncoderImpl::new(&mut writer, bincode::config::standard());
2636 BincodeAdapter(task).encode(&mut encoder).map_err(|error| {
2637 CuError::from(format!("Failed to capture keyframe component: {error}"))
2638 })?;
2639 let payload_len = writer.written;
2640 let payload_len = u32::try_from(payload_len)
2641 .map_err(|_| CuError::from("Keyframe component exceeds u32 size"))?;
2642 frame[..KEYFRAME_FRAME_HEADER_LEN].copy_from_slice(&payload_len.to_le_bytes());
2643 let frame_len = KEYFRAME_FRAME_HEADER_LEN + payload_len as usize;
2644 unsafe {
2645 ptr.add(offset)
2646 .cast::<u64>()
2647 .write_unaligned(frame_len as u64);
2648 }
2649 Ok(frame_len)
2650 }
2651
2652 fn new() -> Self {
2653 Self::from_serialized(0, CuTime::default(), KEYFRAME_PAYLOAD_HEADER.to_vec())
2654 }
2655
2656 fn reset(&mut self, culistid: u64, timestamp: CuTime) {
2658 self.culistid = culistid;
2659 self.timestamp = timestamp;
2660 self.serialized_tasks.clear();
2661 self.serialized_tasks
2662 .extend_from_slice(KEYFRAME_PAYLOAD_HEADER);
2663 }
2664
2665 fn add_frozen_task(&mut self, task: &impl Freezable) -> Result<usize, EncodeError> {
2667 let cfg = bincode::config::standard();
2668 let start = self.serialized_tasks.len();
2669 let payload_offset =
2670 start
2671 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
2672 .ok_or(EncodeError::Other(
2673 "keyframe component frame offset overflow",
2674 ))?;
2675 if payload_offset > self.serialized_tasks.capacity() {
2676 return Err(EncodeError::UnexpectedEnd);
2677 }
2678
2679 self.serialized_tasks.resize(payload_offset, 0);
2680 let length_offset = start;
2681 self.serialized_tasks[length_offset..payload_offset].fill(0);
2682
2683 let mut encoder =
2684 EncoderImpl::<_, _>::new(PreallocatedVecWriter(&mut self.serialized_tasks), cfg);
2685 if let Err(error) = BincodeAdapter(task).encode(&mut encoder) {
2686 self.serialized_tasks.truncate(start);
2687 return Err(error);
2688 }
2689 let payload_len = encoder.into_writer().0.len() - payload_offset;
2690 let payload_len = u32::try_from(payload_len).map_err(|_| {
2691 self.serialized_tasks.truncate(start);
2692 EncodeError::OtherString(
2693 "keyframe component snapshot exceeds the u32 frame limit".to_string(),
2694 )
2695 })?;
2696 self.serialized_tasks
2697 .truncate(payload_offset + payload_len as usize);
2698 self.serialized_tasks[length_offset..payload_offset]
2699 .copy_from_slice(&payload_len.to_le_bytes());
2700 Ok(self.serialized_tasks.len() - start)
2701 }
2702}
2703
2704const KEYFRAME_PAYLOAD_MAGIC: &[u8; 4] = b"CUKF";
2705const KEYFRAME_PAYLOAD_VERSION: u8 = 1;
2706const KEYFRAME_PAYLOAD_HEADER: &[u8; 5] = b"CUKF\x01";
2707const KEYFRAME_FRAME_HEADER_LEN: usize = 4;
2708
2709#[doc(hidden)]
2711pub struct KeyFramePayloadReader<'a> {
2712 remaining: &'a [u8],
2713 distributed: Option<(&'a [u8], usize, usize)>,
2714}
2715
2716impl<'a> KeyFramePayloadReader<'a> {
2717 pub fn new(keyframe: &'a KeyFrame) -> CuResult<Self> {
2719 let payload = keyframe.serialized_tasks.as_slice();
2720 if let Some(count) = distributed_count(payload).map_err(CuError::from)? {
2721 return Ok(Self {
2722 remaining: &[],
2723 distributed: Some((payload, 0, count)),
2724 });
2725 }
2726 if payload.len() < KEYFRAME_PAYLOAD_HEADER.len()
2727 || payload[..KEYFRAME_PAYLOAD_MAGIC.len()] != *KEYFRAME_PAYLOAD_MAGIC
2728 {
2729 return Err(CuError::from(
2730 "Unsupported legacy keyframe payload: expected framed format version 1",
2731 ));
2732 }
2733 let version = payload[KEYFRAME_PAYLOAD_MAGIC.len()];
2734 if version != KEYFRAME_PAYLOAD_VERSION {
2735 return Err(CuError::from(format!(
2736 "Unsupported keyframe payload version {version}; expected {KEYFRAME_PAYLOAD_VERSION}"
2737 )));
2738 }
2739 Ok(Self {
2740 remaining: &payload[KEYFRAME_PAYLOAD_HEADER.len()..],
2741 distributed: None,
2742 })
2743 }
2744
2745 pub fn next_frame(&mut self) -> CuResult<&'a [u8]> {
2747 if let Some((bytes, index, count)) = &mut self.distributed {
2748 if *index >= *count {
2749 return Err(CuError::from("Keyframe ended before next component frame"));
2750 }
2751 let frame = distributed_frame(bytes, *index).map_err(CuError::from)?;
2752 *index += 1;
2753 return Ok(&frame[KEYFRAME_FRAME_HEADER_LEN..]);
2754 }
2755 if self.remaining.len() < KEYFRAME_FRAME_HEADER_LEN {
2756 return Err(CuError::from("Keyframe ended before next component frame"));
2757 }
2758 let payload_len = u32::from_le_bytes(
2759 self.remaining[..KEYFRAME_FRAME_HEADER_LEN]
2760 .try_into()
2761 .map_err(|_| CuError::from("Invalid keyframe component frame length"))?,
2762 ) as usize;
2763 let frame_end = KEYFRAME_FRAME_HEADER_LEN
2764 .checked_add(payload_len)
2765 .ok_or_else(|| CuError::from("Keyframe component frame length overflow"))?;
2766 if frame_end > self.remaining.len() {
2767 return Err(CuError::from("Keyframe component frame is truncated"));
2768 }
2769 let payload = &self.remaining[KEYFRAME_FRAME_HEADER_LEN..frame_end];
2770 self.remaining = &self.remaining[frame_end..];
2771 Ok(payload)
2772 }
2773
2774 pub fn finish(self) -> CuResult<()> {
2776 if let Some((_, index, count)) = self.distributed {
2777 return if index == count {
2778 Ok(())
2779 } else {
2780 Err(CuError::from("Keyframe contains trailing component data"))
2781 };
2782 }
2783 if self.remaining.is_empty() {
2784 Ok(())
2785 } else {
2786 Err(CuError::from("Keyframe contains trailing component data"))
2787 }
2788 }
2789}
2790
2791struct FrameSliceReader<'a> {
2792 remaining: &'a [u8],
2793}
2794
2795impl Reader for FrameSliceReader<'_> {
2796 fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> {
2797 if bytes.len() > self.remaining.len() {
2798 return Err(DecodeError::UnexpectedEnd {
2799 additional: bytes.len() - self.remaining.len(),
2800 });
2801 }
2802 let (read, remaining) = self.remaining.split_at(bytes.len());
2803 bytes.copy_from_slice(read);
2804 self.remaining = remaining;
2805 Ok(())
2806 }
2807
2808 fn peek_read(&mut self, length: usize) -> Option<&[u8]> {
2809 self.remaining.get(..length)
2810 }
2811
2812 fn consume(&mut self, length: usize) {
2813 self.remaining = self.remaining.get(length..).unwrap_or_default();
2814 }
2815}
2816
2817#[doc(hidden)]
2819pub fn thaw_keyframe_component(item: &mut impl Freezable, frame: &[u8]) -> CuResult<()> {
2820 let reader = FrameSliceReader { remaining: frame };
2821 let mut decoder = DecoderImpl::new(reader, bincode::config::standard(), ());
2822 item.thaw(&mut decoder)
2823 .map_err(|error| CuError::from(format!("Failed to thaw keyframe component: {error}")))?;
2824 let trailing = decoder.reader().remaining.len();
2825 if trailing != 0 {
2826 return Err(CuError::from(format!(
2827 "Keyframe component snapshot has {} trailing bytes",
2828 trailing
2829 )));
2830 }
2831 Ok(())
2832}
2833
2834#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2836pub enum RuntimeLifecycleConfigSource {
2837 ProgrammaticOverride,
2838 ExternalFile,
2839 BundledDefault,
2840}
2841
2842#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2844pub struct RuntimeLifecycleStackInfo {
2845 pub app_name: String,
2846 pub app_version: String,
2847 pub git_commit: Option<String>,
2848 pub git_dirty: Option<bool>,
2849 pub subsystem_id: Option<String>,
2850 pub subsystem_code: u16,
2851 pub instance_id: u32,
2852}
2853
2854#[derive(Clone, Copy, Encode, Decode, Debug, PartialEq, Eq)]
2856pub enum RuntimeStopReason {
2857 Requested,
2858 Completed,
2859 Error,
2860 Panic,
2861}
2862
2863#[derive(Clone, Copy, Encode, Decode, Debug, PartialEq, Eq)]
2865pub enum RuntimeLifecycleOperation {
2866 Start,
2867 Stop,
2868 Iteration,
2869 Shutdown,
2870}
2871
2872#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2874pub enum RuntimeLifecycleEvent {
2875 Instantiated {
2876 config_source: RuntimeLifecycleConfigSource,
2877 },
2878 MissionStarted,
2879 MissionStopped {
2880 reason: RuntimeStopReason,
2881 },
2882 LifecycleFailed {
2883 operation: RuntimeLifecycleOperation,
2884 error: String,
2885 },
2886 Panic {
2887 message: String,
2888 file: Option<String>,
2889 line: Option<u32>,
2890 column: Option<u32>,
2891 },
2892 ShutdownCompleted,
2893 #[cfg(feature = "clock-sync")]
2895 ClockSync(crate::clock_sync::ClockSyncRecord),
2896}
2897
2898impl RuntimeLifecycleEvent {
2899 pub fn clock_sync_record(&self) -> Option<&crate::clock_sync::ClockSyncRecord> {
2901 #[cfg(feature = "clock-sync")]
2902 if let Self::ClockSync(record) = self {
2903 return Some(record);
2904 }
2905 None
2906 }
2907}
2908
2909#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2911pub struct RuntimeLifecycleRecord {
2912 pub timestamp: CuTime,
2913 pub event: RuntimeLifecycleEvent,
2914}
2915
2916#[doc(hidden)]
2918pub type RuntimeLifecycleSink = SemanticRecordSink<RuntimeLifecycleRecord>;
2919
2920impl<
2921 CT,
2922 CB,
2923 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2924 M: CuMonitor,
2925 const NBCL: usize,
2926> CuRuntime<CT, CB, P, M, NBCL>
2927{
2928 #[inline]
2932 pub fn record_execution_marker(&self, marker: ExecutionMarker) {
2933 self.execution_probe.record(marker);
2934 }
2935
2936 #[inline]
2941 pub fn execution_probe_ref(&self) -> &RuntimeExecutionProbe {
2942 #[cfg(feature = "std")]
2943 {
2944 self.execution_probe.as_ref()
2945 }
2946
2947 #[cfg(not(feature = "std"))]
2948 {
2949 &self.execution_probe
2950 }
2951 }
2952}
2953
2954#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2959pub enum CuTaskType {
2960 Source,
2961 Regular,
2962 Sink,
2963}
2964
2965impl From<TaskKind> for CuTaskType {
2966 fn from(value: TaskKind) -> Self {
2967 match value {
2968 TaskKind::Source => CuTaskType::Source,
2969 TaskKind::Regular | TaskKind::Stateless => CuTaskType::Regular,
2970 TaskKind::Sink => CuTaskType::Sink,
2971 }
2972 }
2973}
2974
2975#[derive(Debug, Clone)]
2976pub struct CuOutputPack {
2977 pub culist_index: u32,
2978 pub msg_types: Vec<String>,
2979 pub src_channels: Vec<Option<String>>,
2987}
2988
2989#[derive(Debug, Clone)]
2990pub struct CuInputMsg {
2991 pub culist_index: u32,
2992 pub msg_type: String,
2993 pub src_port: usize,
2994 pub edge_id: usize,
2995 pub connection_order: usize,
2996}
2997
2998#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3006pub enum CuStepPhase {
3007 #[default]
3009 Whole,
3010 AnytimeBase,
3013 AnytimeRefine,
3015}
3016
3017pub struct CuExecutionStep {
3019 pub node_id: NodeId,
3021 pub node: Node,
3023 pub task_type: CuTaskType,
3025 pub phase: CuStepPhase,
3028
3029 pub input_msg_indices_types: Vec<CuInputMsg>,
3032
3033 pub output_msg_pack: Option<CuOutputPack>,
3036}
3037
3038impl Debug for CuExecutionStep {
3039 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
3040 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
3041 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
3042 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
3043 f.write_str(format!(" phase: {:?}\n", self.phase).as_str())?;
3044 f.write_str(
3045 format!(
3046 " input_msg_types: {:?}\n",
3047 self.input_msg_indices_types
3048 )
3049 .as_str(),
3050 )?;
3051 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
3052 Ok(())
3053 }
3054}
3055
3056pub struct CuExecutionLoop {
3061 pub steps: Vec<CuExecutionUnit>,
3062 pub loop_count: Option<u32>,
3063}
3064
3065impl Debug for CuExecutionLoop {
3066 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
3067 f.write_str("CuExecutionLoop:\n")?;
3068 for step in &self.steps {
3069 match step {
3070 CuExecutionUnit::Step(step) => {
3071 step.fmt(f)?;
3072 }
3073 CuExecutionUnit::Loop(l) => {
3074 l.fmt(f)?;
3075 }
3076 }
3077 }
3078
3079 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
3080 Ok(())
3081 }
3082}
3083
3084#[derive(Debug)]
3086pub enum CuExecutionUnit {
3087 Step(Box<CuExecutionStep>),
3088 Loop(CuExecutionLoop),
3089}
3090
3091pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
3092 let node = graph
3093 .get_node(node_id)
3094 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
3095
3096 if node.get_flavor() == crate::config::Flavor::Task {
3097 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
3098 }
3099
3100 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
3101 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
3102 Ok(match (has_inputs, has_outputs) {
3103 (false, true) => CuTaskType::Source,
3104 (true, false) => CuTaskType::Sink,
3105 _ => CuTaskType::Regular,
3106 })
3107}
3108
3109pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
3111 let order = topo_bfs_order(graph)?;
3112 check_order(graph, &order)?;
3113 plan_from_order(graph, &order)
3114}
3115
3116pub fn expand_anytime_steps(plan: &mut CuExecutionLoop) -> CuResult<()> {
3134 loop {
3135 let Some(base_pos) = plan.steps.iter().position(|unit| {
3138 matches!(
3139 unit,
3140 CuExecutionUnit::Step(step) if step.phase == CuStepPhase::Whole
3141 && step.node.anytime().is_some()
3142 && !step.node.is_background()
3143 )
3144 }) else {
3145 return Ok(());
3146 };
3147
3148 let CuExecutionUnit::Step(base_step) = &mut plan.steps[base_pos] else {
3149 unreachable!("position() only matches steps");
3150 };
3151 let anytime = base_step
3152 .node
3153 .anytime()
3154 .expect("position() only matches anytime nodes");
3155 let Some(max_refines) = anytime.max_refines else {
3158 return Err(CuError::from(format!(
3159 "Task '{}': a foreground anytime task needs anytime.max_refines to expand into a static plan.",
3160 base_step.node.get_id()
3161 )));
3162 };
3163 base_step.phase = CuStepPhase::AnytimeBase;
3164 let output_pack = base_step.output_msg_pack.clone().ok_or_else(|| {
3165 CuError::from(format!(
3166 "Task '{}': an anytime task needs an output to refine.",
3167 base_step.node.get_id()
3168 ))
3169 })?;
3170 let output_index = output_pack.culist_index;
3171 let node_id = base_step.node_id;
3172 let node = base_step.node.clone();
3173 let task_type = base_step.task_type;
3174
3175 let refine_step = || {
3176 CuExecutionUnit::Step(Box::new(CuExecutionStep {
3177 node_id,
3178 node: node.clone(),
3179 task_type,
3180 phase: CuStepPhase::AnytimeRefine,
3181 input_msg_indices_types: Vec::new(),
3182 output_msg_pack: Some(output_pack.clone()),
3183 }))
3184 };
3185
3186 let consumer_pos = plan.steps[base_pos + 1..]
3189 .iter()
3190 .position(|unit| {
3191 matches!(
3192 unit,
3193 CuExecutionUnit::Step(step) if step
3194 .input_msg_indices_types
3195 .iter()
3196 .any(|input| input.culist_index == output_index)
3197 )
3198 })
3199 .map(|offset| base_pos + 1 + offset)
3200 .unwrap_or(base_pos + 1);
3201
3202 let mut tail = plan.steps.split_off(base_pos + 1);
3203 let suffix = tail.split_off(consumer_pos - base_pos - 1);
3204 let gap = tail;
3205
3206 let mut remaining = max_refines.max(1);
3208 remaining -= 1;
3209 plan.steps.push(refine_step());
3210 for gap_unit in gap {
3211 plan.steps.push(gap_unit);
3212 if remaining > 0 {
3213 remaining -= 1;
3214 plan.steps.push(refine_step());
3215 }
3216 }
3217 for _ in 0..remaining {
3218 plan.steps.push(refine_step());
3219 }
3220 plan.steps.extend(suffix);
3221 }
3222}
3223
3224#[cfg(test)]
3226mod tests {
3227 use super::*;
3228 use crate::config::Node;
3229 use crate::context::CuContext;
3230 use crate::cutask::CuSinkTask;
3231 use crate::cutask::{CuSrcTask, Freezable};
3232 use crate::monitoring::NoMonitor;
3233 use crate::reflect::Reflect;
3234 use bincode::Encode;
3235 use core::cell::Cell;
3236 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
3237 use serde_derive::{Deserialize, Serialize};
3238 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3239 use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
3240 #[cfg(feature = "std")]
3241 use std::sync::{Arc, Mutex};
3242
3243 struct CountingSnapshot<'a> {
3244 calls: &'a Cell<usize>,
3245 value: u32,
3246 fail: bool,
3247 }
3248
3249 impl Freezable for CountingSnapshot<'_> {
3250 fn freeze<E: bincode::enc::Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
3251 self.calls.set(self.calls.get() + 1);
3252 self.value.encode(encoder)?;
3253 if self.fail {
3254 Err(EncodeError::OtherString(
3255 "intentional freeze failure".to_string(),
3256 ))
3257 } else {
3258 Ok(())
3259 }
3260 }
3261 }
3262
3263 #[derive(Default)]
3264 struct SnapshotValue(u32);
3265
3266 impl Freezable for SnapshotValue {
3267 fn thaw<D: bincode::de::Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
3268 self.0 = u32::decode(decoder)?;
3269 Ok(())
3270 }
3271 }
3272
3273 #[test]
3274 fn keyframe_frames_freeze_once_and_roll_back_only_failed_frame() {
3275 let calls = Cell::new(0);
3276 let mut keyframe = KeyFrame::new();
3277 keyframe
3278 .serialized_tasks
3279 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
3280 .unwrap();
3281 keyframe.reset(7, CuTime::from_nanos(70));
3282 keyframe
3283 .add_frozen_task(&CountingSnapshot {
3284 calls: &calls,
3285 value: 11,
3286 fail: false,
3287 })
3288 .unwrap();
3289 let committed_len = keyframe.serialized_tasks.len();
3290
3291 let failing = CountingSnapshot {
3292 calls: &calls,
3293 value: 99,
3294 fail: true,
3295 };
3296 assert!(keyframe.add_frozen_task(&failing).is_err());
3297 assert_eq!(keyframe.serialized_tasks.len(), committed_len);
3298
3299 keyframe
3300 .add_frozen_task(&CountingSnapshot {
3301 calls: &calls,
3302 value: 22,
3303 fail: false,
3304 })
3305 .unwrap();
3306 assert_eq!(calls.get(), 3, "each append must call freeze exactly once");
3307 let first_payload_len = bincode::encode_to_vec(11u32, bincode::config::standard())
3308 .unwrap()
3309 .len();
3310 let second_payload_len = bincode::encode_to_vec(22u32, bincode::config::standard())
3311 .unwrap()
3312 .len();
3313 assert_eq!(
3314 keyframe.serialized_tasks.len(),
3315 KEYFRAME_PAYLOAD_HEADER.len()
3316 + 2 * KEYFRAME_FRAME_HEADER_LEN
3317 + first_payload_len
3318 + second_payload_len,
3319 "component frames carry only a length prefix"
3320 );
3321
3322 let mut frames = KeyFramePayloadReader::new(&keyframe).unwrap();
3323 let mut first = SnapshotValue::default();
3324 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
3325 let mut second = SnapshotValue::default();
3326 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
3327 frames.finish().unwrap();
3328 assert_eq!((first.0, second.0), (11, 22));
3329 }
3330
3331 #[test]
3332 fn distributed_keyframe_preserves_serial_wire_and_restore_order() {
3333 let serial_calls = Cell::new(0);
3334 let mut serial = KeyFrame::new();
3335 serial.serialized_tasks.reserve(256);
3336 serial.reset(17, CuTime::from_nanos(123));
3337 serial
3338 .add_frozen_task(&CountingSnapshot {
3339 calls: &serial_calls,
3340 value: 11,
3341 fail: false,
3342 })
3343 .unwrap();
3344 serial
3345 .add_frozen_task(&CountingSnapshot {
3346 calls: &serial_calls,
3347 value: 22,
3348 fail: false,
3349 })
3350 .unwrap();
3351
3352 let distributed_calls = Cell::new(0);
3353 let mut distributed = KeyFrame::distributed(&[128, 128]);
3354 distributed.reset_distributed(17, CuTime::from_nanos(123), 1);
3355 for component in 0..2 {
3356 let (offset, _) = distributed_region(&distributed.serialized_tasks, component).unwrap();
3357 assert_eq!(
3358 (distributed.serialized_tasks.as_ptr() as usize + offset)
3359 % DISTRIBUTED_REGION_ALIGN,
3360 0,
3361 );
3362 }
3363 let capture_ptr = distributed.serialized_tasks.as_mut_ptr();
3364 let capture_len = distributed.serialized_tasks.len();
3365 unsafe {
3366 KeyFrame::freeze_region(
3367 capture_ptr,
3368 capture_len,
3369 1,
3370 &CountingSnapshot {
3371 calls: &distributed_calls,
3372 value: 22,
3373 fail: false,
3374 },
3375 )
3376 .unwrap();
3377 KeyFrame::freeze_region(
3378 capture_ptr,
3379 capture_len,
3380 0,
3381 &CountingSnapshot {
3382 calls: &distributed_calls,
3383 value: 11,
3384 fail: false,
3385 },
3386 )
3387 .unwrap();
3388 }
3389
3390 let config = bincode::config::standard();
3391 assert_eq!(
3392 bincode::encode_to_vec(&distributed, config).unwrap(),
3393 bincode::encode_to_vec(&serial, config).unwrap(),
3394 );
3395 let mut frames = KeyFramePayloadReader::new(&distributed).unwrap();
3396 let mut first = SnapshotValue::default();
3397 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
3398 let mut second = SnapshotValue::default();
3399 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
3400 frames.finish().unwrap();
3401 assert_eq!((first.0, second.0), (11, 22));
3402 assert_eq!(distributed_calls.get(), 2);
3403 }
3404
3405 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
3406 #[test]
3407 fn arena_reset_and_distributed_capture_do_not_allocate() {
3408 let calls = Cell::new(0);
3409 let mut slots = allocate_slots::<Msgs>(1);
3410 slots[0].prepare_keyframes(&[128]);
3411 let mut slot = slots.pop().unwrap();
3412
3413 let allocations = crate::monitoring::ScopedAllocCounter::new();
3414 slot.reset(3);
3415 slot.keyframe_mut()
3416 .unwrap()
3417 .reset_distributed(3, CuTime::from_nanos(30), 1);
3418 let (capture_ptr, capture_len) = slot.keyframe_capture();
3419 unsafe {
3420 KeyFrame::freeze_region(
3421 capture_ptr,
3422 capture_len,
3423 0,
3424 &CountingSnapshot {
3425 calls: &calls,
3426 value: 42,
3427 fail: false,
3428 },
3429 )
3430 .unwrap();
3431 }
3432
3433 assert_eq!(allocations.allocated(), 0);
3434 assert_eq!(calls.get(), 1);
3435 }
3436
3437 #[cfg(feature = "std")]
3438 #[test]
3439 fn arena_slots_wrap_without_moving_storage() {
3440 let mut slots = allocate_slots::<Msgs>(2);
3441 let pointers = slots
3442 .iter_mut()
3443 .map(CuSlotLease::as_ptr)
3444 .collect::<Vec<_>>();
3445
3446 for generation in 1..=4 {
3447 slots.rotate_left(1);
3448 for slot in &mut slots {
3449 let index = slot.index();
3450 slot.reset((generation * 2 + index) as u64);
3451 assert_eq!(slot.generation(), generation as u64);
3452 assert_eq!(slot.as_ptr(), pointers[index]);
3453 }
3454 }
3455 }
3456
3457 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
3458 #[test]
3459 fn preallocated_keyframe_append_does_not_allocate() {
3460 let calls = Cell::new(0);
3461 let mut keyframe = KeyFrame::new();
3462 keyframe
3463 .serialized_tasks
3464 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
3465 .unwrap();
3466 keyframe.reset(3, CuTime::from_nanos(30));
3467
3468 let allocations = crate::monitoring::ScopedAllocCounter::new();
3469 keyframe
3470 .add_frozen_task(&CountingSnapshot {
3471 calls: &calls,
3472 value: 42,
3473 fail: false,
3474 })
3475 .unwrap();
3476
3477 assert_eq!(allocations.allocated(), 0);
3478 assert_eq!(calls.get(), 1);
3479 }
3480
3481 #[test]
3482 fn keyframe_reader_rejects_legacy_payload_clearly() {
3483 let keyframe = KeyFrame::from_serialized(0, CuTime::default(), vec![0, 1, 2]);
3484 let error = match KeyFramePayloadReader::new(&keyframe) {
3485 Ok(_) => panic!("legacy payload unexpectedly accepted"),
3486 Err(error) => error,
3487 };
3488 assert!(error.to_string().contains("legacy keyframe payload"));
3489 }
3490
3491 #[derive(Reflect)]
3492 pub struct TestSource {}
3493
3494 impl Freezable for TestSource {}
3495
3496 impl CuSrcTask for TestSource {
3497 type Resources<'r> = ();
3498 type Output<'m> = ();
3499 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
3500 where
3501 Self: Sized,
3502 {
3503 Ok(Self {})
3504 }
3505
3506 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
3507 Ok(())
3508 }
3509 }
3510
3511 #[derive(Reflect)]
3512 pub struct TestSink {}
3513
3514 impl Freezable for TestSink {}
3515
3516 impl CuSinkTask for TestSink {
3517 type Resources<'r> = ();
3518 type Input<'m> = ();
3519
3520 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
3521 where
3522 Self: Sized,
3523 {
3524 Ok(Self {})
3525 }
3526
3527 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
3528 Ok(())
3529 }
3530 }
3531
3532 type Tasks = (TestSource, TestSink);
3534 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
3535 const TEST_NBCL: usize = 2;
3536
3537 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
3538 struct Msgs(());
3539
3540 impl ErasedCuStampedDataSet for Msgs {
3541 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3542 Vec::new()
3543 }
3544 }
3545
3546 impl MatchingTasks for Msgs {
3547 fn get_all_task_ids() -> &'static [&'static str] {
3548 &[]
3549 }
3550 }
3551
3552 impl CuListZeroedInit for Msgs {
3553 fn init_zeroed(&mut self) {}
3554 }
3555
3556 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
3557 struct IntMsgs(i32);
3558
3559 impl ErasedCuStampedDataSet for IntMsgs {
3560 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3561 Vec::new()
3562 }
3563 }
3564
3565 impl MatchingTasks for IntMsgs {
3566 fn get_all_task_ids() -> &'static [&'static str] {
3567 &[]
3568 }
3569 }
3570
3571 impl CuListZeroedInit for IntMsgs {
3572 fn init_zeroed(&mut self) {}
3573 }
3574
3575 #[cfg(not(feature = "async-cl-io"))]
3576 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
3577 struct DistributedIntMsgs(i32);
3578
3579 #[cfg(not(feature = "async-cl-io"))]
3580 impl ErasedCuStampedDataSet for DistributedIntMsgs {
3581 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3582 Vec::new()
3583 }
3584 }
3585
3586 #[cfg(not(feature = "async-cl-io"))]
3587 impl MatchingTasks for DistributedIntMsgs {
3588 fn get_all_task_ids() -> &'static [&'static str] {
3589 &[]
3590 }
3591 }
3592
3593 #[cfg(not(feature = "async-cl-io"))]
3594 impl CuListZeroedInit for DistributedIntMsgs {
3595 const DISTRIBUTED: bool = true;
3596
3597 fn init_zeroed(&mut self) {}
3598 }
3599
3600 #[cfg(feature = "std")]
3601 fn tasks_instanciator(
3602 all_instances_configs: Vec<Option<&ComponentConfig>>,
3603 _resources: &mut ResourceManager,
3604 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
3605 ) -> CuResult<Tasks> {
3606 Ok((
3607 TestSource::new(all_instances_configs[0], ())?,
3608 TestSink::new(all_instances_configs[1], ())?,
3609 ))
3610 }
3611
3612 #[cfg(not(feature = "std"))]
3613 fn tasks_instanciator(
3614 all_instances_configs: Vec<Option<&ComponentConfig>>,
3615 _resources: &mut ResourceManager,
3616 ) -> CuResult<Tasks> {
3617 Ok((
3618 TestSource::new(all_instances_configs[0], ())?,
3619 TestSink::new(all_instances_configs[1], ())?,
3620 ))
3621 }
3622
3623 fn monitor_instanciator(
3624 _config: &CuConfig,
3625 metadata: CuMonitoringMetadata,
3626 runtime: CuMonitoringRuntime,
3627 ) -> NoMonitor {
3628 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
3629 }
3630
3631 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
3632 Ok(())
3633 }
3634
3635 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
3636 Ok(ResourceManager::new(&[]))
3637 }
3638
3639 #[derive(Debug)]
3640 struct FakeWriter {}
3641
3642 impl<E: Encode> WriteStream<E> for FakeWriter {
3643 fn log(&mut self, _obj: &E) -> CuResult<()> {
3644 Ok(())
3645 }
3646 }
3647
3648 #[cfg(not(feature = "async-cl-io"))]
3649 #[derive(Debug)]
3650 struct RecordingSyncWriter {
3651 ids: Arc<Mutex<Vec<u64>>>,
3652 last_log_bytes: usize,
3653 fail_on: Option<u64>,
3654 }
3655
3656 #[cfg(not(feature = "async-cl-io"))]
3657 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
3658 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
3659 self.ids.lock().unwrap().push(culist.id);
3660 if self.fail_on == Some(culist.id) {
3661 return Err(CuError::from(format!(
3662 "logger failed for CopperList #{}",
3663 culist.id
3664 )));
3665 }
3666 Ok(())
3667 }
3668
3669 fn last_log_bytes(&self) -> Option<usize> {
3670 Some(self.last_log_bytes)
3671 }
3672 }
3673
3674 #[cfg(feature = "std")]
3675 #[derive(Debug)]
3676 struct RecordingSemanticSink {
3677 ids: Arc<Mutex<Vec<u64>>>,
3678 }
3679
3680 #[cfg(feature = "std")]
3681 impl WriteStream<CopperList<IntMsgs>> for RecordingSemanticSink {
3682 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
3683 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
3684 self.ids.lock().unwrap().push(culist.id);
3685 Ok(())
3686 }
3687
3688 fn last_log_bytes(&self) -> Option<usize> {
3689 Some(23)
3690 }
3691 }
3692
3693 #[cfg(feature = "std")]
3694 impl WriteStream<CopperList<Msgs>> for RecordingSemanticSink {
3695 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
3696 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
3697 self.ids.lock().unwrap().push(culist.id);
3698 Ok(())
3699 }
3700
3701 fn last_log_bytes(&self) -> Option<usize> {
3702 Some(23)
3703 }
3704 }
3705
3706 #[cfg(feature = "std")]
3707 #[derive(Debug)]
3708 struct RecordingKeyFrameSink {
3709 ids: Arc<Mutex<Vec<u64>>>,
3710 }
3711
3712 #[cfg(feature = "std")]
3713 impl WriteStream<KeyFrame> for RecordingKeyFrameSink {
3714 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
3715 self.ids.lock().unwrap().push(keyframe.culistid);
3716 Ok(())
3717 }
3718
3719 fn last_log_bytes(&self) -> Option<usize> {
3720 Some(29)
3721 }
3722 }
3723
3724 #[test]
3725 fn test_runtime_instantiation() {
3726 let mut config = CuConfig::default();
3727 let graph = config.get_graph_mut(None).unwrap();
3728 graph.add_node(Node::new("a", "TestSource")).unwrap();
3729 graph.add_node(Node::new("b", "TestSink")).unwrap();
3730 graph.connect(0, 1, "()").unwrap();
3731 let runtime: CuResult<TestRuntime> =
3732 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3733 RobotClock::default(),
3734 &config,
3735 crate::config::DEFAULT_MISSION_ID,
3736 CuRuntimeParts::new(
3737 tasks_instanciator,
3738 &[],
3739 &[],
3740 #[cfg(feature = "std")]
3741 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3742 monitor_instanciator,
3743 bridges_instanciator,
3744 ),
3745 FakeWriter {},
3746 FakeWriter {},
3747 OutputRequirements::new(true, true),
3748 )
3749 .try_with_resources_instantiator(resources_instanciator)
3750 .and_then(|builder| builder.build());
3751 assert!(runtime.is_ok());
3752 }
3753
3754 #[cfg(feature = "std")]
3755 #[test]
3756 fn downstream_requirements_are_independent_from_local_logging() {
3757 let mut config = CuConfig::default();
3758 config.logging = Some(crate::config::LoggingConfig {
3759 enable_task_logging: false,
3760 enable_keyframe_logging: false,
3761 keyframe_interval: Some(1),
3762 ..Default::default()
3763 });
3764 let graph = config.get_graph_mut(None).unwrap();
3765 graph.add_node(Node::new("a", "TestSource")).unwrap();
3766 graph.add_node(Node::new("b", "TestSink")).unwrap();
3767 graph.connect(0, 1, "()").unwrap();
3768
3769 let copperlist_ids = Arc::new(Mutex::new(Vec::new()));
3770 let keyframe_ids = Arc::new(Mutex::new(Vec::new()));
3771 let mut runtime: TestRuntime =
3772 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3773 RobotClock::default(),
3774 &config,
3775 crate::config::DEFAULT_MISSION_ID,
3776 CuRuntimeParts::new(
3777 tasks_instanciator,
3778 &[],
3779 &[],
3780 #[cfg(feature = "std")]
3781 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3782 monitor_instanciator,
3783 bridges_instanciator,
3784 ),
3785 RecordingSemanticSink {
3786 ids: copperlist_ids.clone(),
3787 },
3788 RecordingKeyFrameSink {
3789 ids: keyframe_ids.clone(),
3790 },
3791 OutputRequirements::new(true, false).union(OutputRequirements::new(false, true)),
3792 )
3793 .try_with_resources_instantiator(resources_instanciator)
3794 .and_then(|builder| builder.build())
3795 .unwrap();
3796
3797 let copperlist = runtime.copperlists_manager.create().unwrap();
3798 copperlist.change_state(CopperListState::Processing);
3799 runtime.copperlists_manager.end_of_processing(0).unwrap();
3800 runtime.copperlists_manager.finish_pending().unwrap();
3801
3802 runtime
3803 .keyframes_manager
3804 .try_reset(0, &runtime.clock)
3805 .unwrap();
3806 runtime.keyframes_manager.end_of_processing(0).unwrap();
3807 runtime.keyframes_manager.finish_pending().unwrap();
3808
3809 assert_eq!(*copperlist_ids.lock().unwrap(), vec![0]);
3810 assert_eq!(*keyframe_ids.lock().unwrap(), vec![0]);
3811 }
3812
3813 #[test]
3814 fn test_rate_target_period_rejects_zero() {
3815 let err = rate_target_period(0).expect_err("zero rate target should fail");
3816 assert!(
3817 err.to_string()
3818 .contains("Runtime rate target cannot be zero"),
3819 "unexpected error: {err}"
3820 );
3821 }
3822
3823 #[test]
3824 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
3825 let (clock, mock) = RobotClock::mock();
3826 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3827 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
3828
3829 mock.set_value(10_000_000);
3830 limiter.mark_tick(&clock);
3831
3832 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
3833 }
3834
3835 #[test]
3836 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
3837 let (clock, mock) = RobotClock::mock();
3838 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3839
3840 mock.set_value(35_000_000);
3841 limiter.mark_tick(&clock);
3842
3843 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
3844 }
3845
3846 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
3847 #[test]
3848 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
3849 let (clock, _) = RobotClock::mock();
3850 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
3851 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
3852
3853 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
3854 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
3855 }
3856
3857 #[cfg(not(feature = "async-cl-io"))]
3858 #[test]
3859 fn test_copperlists_manager_lifecycle() {
3860 let mut config = CuConfig::default();
3861 let graph = config.get_graph_mut(None).unwrap();
3862 graph.add_node(Node::new("a", "TestSource")).unwrap();
3863 graph.add_node(Node::new("b", "TestSink")).unwrap();
3864 graph.connect(0, 1, "()").unwrap();
3865
3866 let mut runtime: TestRuntime =
3867 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3868 RobotClock::default(),
3869 &config,
3870 crate::config::DEFAULT_MISSION_ID,
3871 CuRuntimeParts::new(
3872 tasks_instanciator,
3873 &[],
3874 &[],
3875 #[cfg(feature = "std")]
3876 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3877 monitor_instanciator,
3878 bridges_instanciator,
3879 ),
3880 FakeWriter {},
3881 FakeWriter {},
3882 OutputRequirements::new(true, true),
3883 )
3884 .try_with_resources_instantiator(resources_instanciator)
3885 .and_then(|builder| builder.build())
3886 .unwrap();
3887
3888 {
3890 let copperlists = &mut runtime.copperlists_manager;
3891 let culist0 = copperlists
3892 .create()
3893 .expect("Ran out of space for copper lists");
3894 let id = culist0.id;
3895 assert_eq!(id, 0);
3896 culist0.change_state(CopperListState::Processing);
3897 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3898 }
3899
3900 {
3901 let copperlists = &mut runtime.copperlists_manager;
3902 let culist1 = copperlists
3903 .create()
3904 .expect("Ran out of space for copper lists");
3905 let id = culist1.id;
3906 assert_eq!(id, 1);
3907 culist1.change_state(CopperListState::Processing);
3908 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3909 }
3910
3911 {
3912 let copperlists = &mut runtime.copperlists_manager;
3913 let culist2 = copperlists.create();
3914 assert!(culist2.is_err());
3915 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3916 let _ = copperlists.end_of_processing(1);
3918 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3919 }
3920
3921 {
3923 let copperlists = &mut runtime.copperlists_manager;
3924 let culist2 = copperlists
3925 .create()
3926 .expect("Ran out of space for copper lists");
3927 let id = culist2.id;
3928 assert_eq!(id, 2);
3929 culist2.change_state(CopperListState::Processing);
3930 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3931 let _ = copperlists.end_of_processing(0);
3933 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3935
3936 let _ = copperlists.end_of_processing(2);
3938 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3941 }
3942 }
3943
3944 #[cfg(not(feature = "async-cl-io"))]
3945 #[test]
3946 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
3947 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3948
3949 assert_eq!(copperlists.next_cl_id(), 0);
3950 assert_eq!(copperlists.last_cl_id(), 0);
3951 assert!(copperlists.peek().is_none());
3952
3953 {
3954 let culist = copperlists.create().unwrap();
3955 culist.msgs.0 = 11;
3956 assert_eq!(culist.id, 0);
3957 assert_eq!(culist.get_state(), CopperListState::Initialized);
3958 }
3959
3960 assert_eq!(copperlists.next_cl_id(), 1);
3961 assert_eq!(copperlists.last_cl_id(), 0);
3962 let peeked = copperlists.peek().unwrap();
3963 assert_eq!(peeked.id, 0);
3964 assert_eq!(peeked.msgs.0, 11);
3965 assert_eq!(peeked.get_state(), CopperListState::Initialized);
3966 }
3967
3968 #[cfg(not(feature = "async-cl-io"))]
3969 #[test]
3970 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
3971 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3972
3973 {
3974 let culist = copperlists.create().unwrap();
3975 culist.msgs.0 = 41;
3976 culist.change_state(CopperListState::Processing);
3977 assert_eq!(culist.id, 0);
3978 }
3979
3980 copperlists.end_of_processing(0).unwrap();
3981 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3982
3983 let reused = copperlists.create().unwrap();
3984 assert_eq!(reused.id, 1);
3985 assert_eq!(reused.get_state(), CopperListState::Initialized);
3986 assert_eq!(reused.msgs.0, 41);
3987 }
3988
3989 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
3990 #[test]
3991 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
3992 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
3993 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3994
3995 {
3996 let culist = copperlists.create().unwrap();
3997 culist.msgs.0 = 10;
3998 culist.change_state(CopperListState::Processing);
3999 }
4000 {
4001 let culist = copperlists.create().unwrap();
4002 culist.msgs.0 = 20;
4003 culist.change_state(CopperListState::Processing);
4004 }
4005
4006 let _ = copperlists.end_of_processing(99);
4007 }
4008
4009 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
4010 #[test]
4011 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
4012 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
4013 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
4014
4015 {
4016 let culist = copperlists.create().unwrap();
4017 culist.msgs.0 = 10;
4018 assert_eq!(culist.get_state(), CopperListState::Initialized);
4019 }
4020
4021 let _ = copperlists.end_of_processing(0);
4022 }
4023
4024 #[cfg(not(feature = "async-cl-io"))]
4025 #[test]
4026 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
4027 let ids = Arc::new(Mutex::new(Vec::new()));
4028 let mut copperlists =
4029 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
4030 ids: ids.clone(),
4031 last_log_bytes: 17,
4032 fail_on: None,
4033 })))
4034 .unwrap();
4035
4036 {
4037 let culist = copperlists.create().unwrap();
4038 culist.msgs.0 = 10;
4039 culist.change_state(CopperListState::Processing);
4040 }
4041 {
4042 let culist = copperlists.create().unwrap();
4043 culist.msgs.0 = 20;
4044 culist.change_state(CopperListState::Processing);
4045 }
4046
4047 copperlists.end_of_processing(0).unwrap();
4048 assert!(ids.lock().unwrap().is_empty());
4049 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
4050
4051 copperlists.end_of_processing(1).unwrap();
4052
4053 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
4054 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
4055 }
4056
4057 #[cfg(not(feature = "async-cl-io"))]
4058 #[test]
4059 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
4060 let ids = Arc::new(Mutex::new(Vec::new()));
4061 let mut copperlists =
4062 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
4063 ids: ids.clone(),
4064 last_log_bytes: 17,
4065 fail_on: None,
4066 })))
4067 .unwrap();
4068 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
4069
4070 {
4071 let culist = copperlists.create().unwrap();
4072 culist.msgs.0 = 10;
4073 culist.change_state(CopperListState::Processing);
4074 }
4075
4076 {
4077 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
4078 capture.select_slot(0);
4079 crate::monitoring::record_payload_handle_bytes(32);
4080 }
4081
4082 copperlists.end_of_processing(0).unwrap();
4083
4084 assert_eq!(*ids.lock().unwrap(), vec![0]);
4085 assert_eq!(copperlists.last_encoded_bytes, 17);
4086 assert_eq!(copperlists.last_handle_bytes, 32);
4087 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4088 }
4089
4090 #[cfg(feature = "std")]
4091 #[test]
4092 fn test_sync_manager_accepts_nonserializing_semantic_sink() {
4093 let ids = Arc::new(Mutex::new(Vec::new()));
4094 let mut copperlists =
4095 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSemanticSink {
4096 ids: ids.clone(),
4097 })))
4098 .unwrap();
4099
4100 let culist = copperlists.create().unwrap();
4101 culist.change_state(CopperListState::Processing);
4102 copperlists.end_of_processing(0).unwrap();
4103
4104 assert_eq!(*ids.lock().unwrap(), vec![0]);
4105 assert_eq!(copperlists.last_encoded_bytes, 23);
4106 assert_eq!(copperlists.last_handle_bytes, 0);
4107 }
4108
4109 #[cfg(feature = "std")]
4110 #[test]
4111 fn test_keyframe_manager_accepts_nonserializing_semantic_sink() {
4112 let ids = Arc::new(Mutex::new(Vec::new()));
4113 let mut keyframes = KeyFramesManager::new(
4114 Some(Box::new(RecordingKeyFrameSink { ids: ids.clone() })),
4115 1,
4116 )
4117 .unwrap();
4118
4119 keyframes.try_reset(7, &RobotClock::default()).unwrap();
4120 keyframes.end_of_processing(7).unwrap();
4121 keyframes.finish_pending().unwrap();
4122
4123 assert_eq!(*ids.lock().unwrap(), vec![7]);
4124 assert_eq!(keyframes.last_encoded_bytes, 29);
4125 }
4126
4127 #[cfg(not(feature = "async-cl-io"))]
4128 #[test]
4129 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
4130 let ids = Arc::new(Mutex::new(Vec::new()));
4131 let mut copperlists =
4132 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
4133 ids: ids.clone(),
4134 last_log_bytes: 17,
4135 fail_on: Some(0),
4136 })))
4137 .unwrap();
4138
4139 {
4140 let culist = copperlists.create().unwrap();
4141 culist.change_state(CopperListState::Processing);
4142 }
4143
4144 let err = copperlists.end_of_processing(0).unwrap_err();
4145
4146 assert!(
4147 err.to_string().contains("logger failed for CopperList #0"),
4148 "unexpected error: {err}"
4149 );
4150 assert_eq!(*ids.lock().unwrap(), vec![0]);
4151 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
4152 assert_eq!(copperlists.last_encoded_bytes, 0);
4153 assert_eq!(copperlists.last_handle_bytes, 0);
4154
4155 let peeked = copperlists.peek().unwrap();
4156 assert_eq!(peeked.id, 0);
4157 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
4158 }
4159
4160 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
4161 #[test]
4162 #[should_panic(expected = "sync slot completion expected CopperList #7 to be Processing")]
4163 fn test_sync_submit_slot_wrong_state_panics_in_debug() {
4164 let mut copperlists = SyncCopperListsManager::<DistributedIntMsgs, 1>::new(None).unwrap();
4165 let mut culist = copperlists.acquire_iteration_slot().unwrap();
4166 culist.reset(7);
4167
4168 let _ = copperlists.submit_slot(culist);
4169 }
4170
4171 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4172 #[derive(Debug, Default)]
4173 struct RecordingWriter {
4174 ids: Arc<Mutex<Vec<u64>>>,
4175 }
4176
4177 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4178 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
4179 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
4180 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
4181 self.ids.lock().unwrap().push(culist.id);
4182 std::thread::sleep(std::time::Duration::from_millis(2));
4183 Ok(())
4184 }
4185 }
4186
4187 #[cfg(feature = "std")]
4188 #[derive(Debug)]
4189 struct OrderedCopperListSink {
4190 events: Arc<Mutex<Vec<&'static str>>>,
4191 }
4192
4193 #[cfg(feature = "std")]
4194 impl WriteStream<CopperList<Msgs>> for OrderedCopperListSink {
4195 fn log(&mut self, _culist: &CopperList<Msgs>) -> CuResult<()> {
4196 self.events.lock().unwrap().push("copperlist");
4197 Ok(())
4198 }
4199 }
4200
4201 #[cfg(feature = "std")]
4202 #[derive(Debug)]
4203 struct OrderedKeyFrameSink {
4204 events: Arc<Mutex<Vec<&'static str>>>,
4205 }
4206
4207 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4208 #[derive(Debug)]
4209 struct BlockingKeyFrameSink {
4210 events: Arc<Mutex<Vec<&'static str>>>,
4211 started: SyncSender<()>,
4212 release: Arc<Mutex<Receiver<()>>>,
4213 }
4214
4215 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4216 impl WriteStream<KeyFrame> for BlockingKeyFrameSink {
4217 fn log(&mut self, _keyframe: &KeyFrame) -> CuResult<()> {
4218 self.events.lock().unwrap().push("keyframe");
4219 self.started
4220 .send(())
4221 .map_err(|_| CuError::from("failed to signal keyframe writer start"))?;
4222 self.release
4223 .lock()
4224 .unwrap()
4225 .recv()
4226 .map_err(|_| CuError::from("failed to release keyframe writer"))
4227 }
4228 }
4229
4230 #[cfg(feature = "std")]
4231 impl WriteStream<KeyFrame> for OrderedKeyFrameSink {
4232 fn log(&mut self, _keyframe: &KeyFrame) -> CuResult<()> {
4233 self.events.lock().unwrap().push("keyframe");
4234 Ok(())
4235 }
4236 }
4237
4238 #[cfg(feature = "std")]
4239 #[test]
4240 fn slot_output_writes_keyframe_then_copperlist_before_recycling() {
4241 let events = Arc::new(Mutex::new(Vec::new()));
4242 let mut manager = SyncCopperListsManager::<Msgs, 1>::new_with_keyframes(
4243 Some(Box::new(OrderedCopperListSink {
4244 events: Arc::clone(&events),
4245 })),
4246 Some(Box::new(OrderedKeyFrameSink {
4247 events: Arc::clone(&events),
4248 })),
4249 )
4250 .unwrap();
4251 let mut slot = allocate_slots::<Msgs>(1).pop().unwrap();
4252 slot.prepare_keyframes(&[128]);
4253 slot.reset(0);
4254 slot.keyframe_mut()
4255 .unwrap()
4256 .reset_distributed(0, CuTime::default(), 1);
4257 let (capture_ptr, capture_len) = slot.keyframe_capture();
4258 unsafe {
4259 KeyFrame::freeze_region(capture_ptr, capture_len, 0, &SnapshotValue(42)).unwrap();
4260 }
4261 slot.change_state(CopperListState::Processing);
4262 let index = slot.index();
4263 let generation = slot.generation();
4264
4265 let recycled = match manager.submit_slot(slot).unwrap() {
4266 OwnedCopperListSubmission::Recycled(slot) => slot,
4267 OwnedCopperListSubmission::Pending => panic!("sync output retained slot"),
4268 };
4269
4270 assert_eq!(*events.lock().unwrap(), vec!["keyframe", "copperlist"]);
4271 assert_eq!(recycled.index(), index);
4272 assert_eq!(recycled.generation(), generation);
4273 assert_eq!(recycled.get_state(), CopperListState::Free);
4274 }
4275
4276 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4277 #[test]
4278 fn async_slot_is_reclaimed_only_after_keyframe_and_copperlist_output() {
4279 let events = Arc::new(Mutex::new(Vec::new()));
4280 let (started_tx, started_rx) = sync_channel(1);
4281 let (release_tx, release_rx) = sync_channel(1);
4282 let mut manager = AsyncCopperListsManager::<Msgs, 2>::new_with_keyframes(
4283 Some(Box::new(OrderedCopperListSink {
4284 events: Arc::clone(&events),
4285 })),
4286 Some(Box::new(BlockingKeyFrameSink {
4287 events: Arc::clone(&events),
4288 started: started_tx,
4289 release: Arc::new(Mutex::new(release_rx)),
4290 })),
4291 )
4292 .unwrap();
4293 manager.prepare_keyframes(&[128]);
4294 let mut slot = manager.acquire_iteration_slot().unwrap();
4295 slot.reset(0);
4296 slot.keyframe_mut()
4297 .unwrap()
4298 .reset_distributed(0, CuTime::default(), 1);
4299 let (capture_ptr, capture_len) = slot.keyframe_capture();
4300 unsafe {
4301 KeyFrame::freeze_region(capture_ptr, capture_len, 0, &SnapshotValue(42)).unwrap();
4302 }
4303 slot.change_state(CopperListState::Processing);
4304 let index = slot.index();
4305 let generation = slot.generation();
4306
4307 assert!(matches!(
4308 manager.submit_slot(slot).unwrap(),
4309 OwnedCopperListSubmission::Pending
4310 ));
4311 started_rx
4312 .recv_timeout(std::time::Duration::from_secs(1))
4313 .unwrap();
4314 assert!(manager.try_reclaim_slot().unwrap().is_none());
4315 assert_eq!(*events.lock().unwrap(), vec!["keyframe"]);
4316
4317 release_tx.send(()).unwrap();
4318 let recycled = manager.wait_reclaim_slot().unwrap();
4319 assert_eq!(*events.lock().unwrap(), vec!["keyframe", "copperlist"]);
4320 assert_eq!(recycled.index(), index);
4321 assert_eq!(recycled.generation(), generation);
4322 assert_eq!(recycled.get_state(), CopperListState::Free);
4323 }
4324
4325 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4326 #[derive(Debug)]
4327 struct BlockingWriter {
4328 ids: Arc<Mutex<Vec<u64>>>,
4329 started: SyncSender<()>,
4330 release: Arc<Mutex<Receiver<()>>>,
4331 }
4332
4333 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4334 impl WriteStream<CopperList<Msgs>> for BlockingWriter {
4335 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
4336 self.ids.lock().unwrap().push(culist.id);
4337 self.started
4338 .send(())
4339 .map_err(|_| CuError::from("failed to signal blocking writer start"))?;
4340 self.release
4341 .lock()
4342 .unwrap()
4343 .recv()
4344 .map_err(|_| CuError::from("failed to release blocking writer"))
4345 }
4346 }
4347
4348 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4349 #[derive(Debug)]
4350 struct BlockingKeyFrameWriter {
4351 ids: Arc<Mutex<Vec<u64>>>,
4352 started: SyncSender<()>,
4353 release: Arc<Mutex<Receiver<()>>>,
4354 }
4355
4356 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4357 impl WriteStream<KeyFrame> for BlockingKeyFrameWriter {
4358 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
4359 self.ids.lock().unwrap().push(keyframe.culistid);
4360 self.started
4361 .send(())
4362 .map_err(|_| CuError::from("failed to signal blocking keyframe writer start"))?;
4363 self.release
4364 .lock()
4365 .unwrap()
4366 .recv()
4367 .map_err(|_| CuError::from("failed to release blocking keyframe writer"))
4368 }
4369 }
4370
4371 #[test]
4372 fn disabled_keyframes_allocate_no_capture_buffers() {
4373 let keyframes = KeyFramesManager::new(None, 1).unwrap();
4374
4375 assert!(keyframes.inner.is_none());
4376 assert!(!keyframes.captures_keyframe(0));
4377 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4378 {
4379 assert!(keyframes.spares.is_empty());
4380 assert!(keyframes.pending_producer.is_none());
4381 assert!(keyframes.worker_handle.is_none());
4382 }
4383 }
4384
4385 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
4386 #[test]
4387 fn disabled_keyframe_manager_allocates_nothing() {
4388 let allocations = crate::monitoring::ScopedAllocCounter::new();
4389 let keyframes = KeyFramesManager::new(None, 1).unwrap();
4390
4391 assert_eq!(allocations.allocated(), 0);
4392 assert!(keyframes.inner.is_none());
4393 }
4394
4395 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4396 #[test]
4397 fn saturated_keyframe_handoff_skips_capture_before_freezing() {
4398 let ids = Arc::new(Mutex::new(Vec::new()));
4399 let (started_tx, started_rx) = sync_channel(1);
4400 let (release_tx, release_rx) = sync_channel(1);
4401 let mut keyframes = KeyFramesManager::new(
4402 Some(Box::new(BlockingKeyFrameWriter {
4403 ids: ids.clone(),
4404 started: started_tx,
4405 release: Arc::new(Mutex::new(release_rx)),
4406 })),
4407 1,
4408 )
4409 .unwrap();
4410 let freeze_calls = Cell::new(0);
4411 let snapshot = CountingSnapshot {
4412 calls: &freeze_calls,
4413 value: 42,
4414 fail: false,
4415 };
4416 keyframes.begin_capture_preallocation();
4417 keyframes.include_capture_capacity(&snapshot).unwrap();
4418 keyframes.finish_capture_preallocation().unwrap();
4419 freeze_calls.set(0);
4420
4421 keyframes.try_reset(0, &RobotClock::default()).unwrap();
4422 assert_ne!(keyframes.freeze_task(0, &snapshot).unwrap(), 0);
4423 keyframes.end_of_processing(0).unwrap();
4424 started_rx
4425 .recv_timeout(std::time::Duration::from_secs(1))
4426 .unwrap();
4427
4428 keyframes.try_reset(1, &RobotClock::default()).unwrap();
4429 assert_ne!(keyframes.freeze_task(1, &snapshot).unwrap(), 0);
4430 keyframes.end_of_processing(1).unwrap();
4431
4432 keyframes.try_reset(2, &RobotClock::default()).unwrap();
4433 assert_eq!(keyframes.freeze_task(2, &snapshot).unwrap(), 0);
4434 keyframes.end_of_processing(2).unwrap();
4435
4436 assert_eq!(freeze_calls.get(), 2);
4437 assert_eq!(keyframes.dropped_keyframes_total(), 1);
4438 assert_eq!(*ids.lock().unwrap(), vec![0]);
4439
4440 release_tx.send(()).unwrap();
4441 started_rx
4442 .recv_timeout(std::time::Duration::from_secs(1))
4443 .unwrap();
4444 release_tx.send(()).unwrap();
4445 keyframes.finish_pending().unwrap();
4446 assert_eq!(*ids.lock().unwrap(), vec![0, 1]);
4447 }
4448
4449 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4450 #[test]
4451 fn test_async_copperlists_manager_flushes_in_order() {
4452 let ids = Arc::new(Mutex::new(Vec::new()));
4453 let mut copperlists = CopperListsManager::<Msgs, 5>::new(Some(Box::new(RecordingWriter {
4454 ids: ids.clone(),
4455 })))
4456 .unwrap();
4457
4458 for expected_id in 0..4 {
4459 let culist = copperlists.create().unwrap();
4460 assert_eq!(culist.id, expected_id);
4461 culist.change_state(CopperListState::Processing);
4462 copperlists.end_of_processing(expected_id).unwrap();
4463 }
4464
4465 copperlists.finish_pending().unwrap();
4466 assert_eq!(copperlists.available_copper_lists().unwrap(), 5);
4467 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
4468 assert_eq!(copperlists.dropped_copperlists_total(), 0);
4469 }
4470
4471 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4472 #[test]
4473 fn test_async_handoff_drops_without_exhausting_execution_slot() {
4474 let ids = Arc::new(Mutex::new(Vec::new()));
4475 let (started_tx, started_rx) = sync_channel(1);
4476 let (release_tx, release_rx) = sync_channel(1);
4477 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
4478 ids: ids.clone(),
4479 started: started_tx,
4480 release: Arc::new(Mutex::new(release_rx)),
4481 })))
4482 .unwrap();
4483
4484 let first = copperlists.create().unwrap();
4485 first.change_state(CopperListState::Processing);
4486 copperlists.end_of_processing(0).unwrap();
4487 started_rx
4488 .recv_timeout(std::time::Duration::from_secs(1))
4489 .unwrap();
4490
4491 let second = copperlists.create().unwrap();
4492 second.change_state(CopperListState::Processing);
4493 copperlists.end_of_processing(1).unwrap();
4494
4495 assert_eq!(copperlists.dropped_copperlists_total(), 1);
4496 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4497 assert_eq!(*ids.lock().unwrap(), vec![0]);
4498
4499 release_tx.send(()).unwrap();
4500 copperlists.finish_pending().unwrap();
4501 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
4502 }
4503
4504 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4505 #[test]
4506 fn test_async_slot_handoff_returns_dropped_copperlist_to_caller() {
4507 let ids = Arc::new(Mutex::new(Vec::new()));
4508 let (started_tx, started_rx) = sync_channel(1);
4509 let (release_tx, release_rx) = sync_channel(1);
4510 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
4511 ids: ids.clone(),
4512 started: started_tx,
4513 release: Arc::new(Mutex::new(release_rx)),
4514 })))
4515 .unwrap();
4516
4517 let mut first = copperlists.acquire_iteration_slot().unwrap();
4518 first.reset(0);
4519 first.change_state(CopperListState::Processing);
4520 assert!(matches!(
4521 copperlists.submit_slot(first).unwrap(),
4522 OwnedCopperListSubmission::Pending
4523 ));
4524 started_rx
4525 .recv_timeout(std::time::Duration::from_secs(1))
4526 .unwrap();
4527
4528 let mut second = copperlists.acquire_iteration_slot().unwrap();
4529 second.reset(1);
4530 second.change_state(CopperListState::Processing);
4531 let recycled = match copperlists.submit_slot(second).unwrap() {
4532 OwnedCopperListSubmission::Recycled(culist) => culist,
4533 OwnedCopperListSubmission::Pending => panic!("saturated handoff accepted CopperList"),
4534 };
4535
4536 assert_eq!(recycled.id, 1);
4537 assert_eq!(recycled.get_state(), CopperListState::Free);
4538 assert_eq!(copperlists.dropped_copperlists_total(), 1);
4539 assert_eq!(*ids.lock().unwrap(), vec![0]);
4540
4541 release_tx.send(()).unwrap();
4542 let completed = copperlists.finish_pending_slots().unwrap();
4543 assert_eq!(completed.len(), 1);
4544 assert_eq!(completed[0].id, 0);
4545 }
4546
4547 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4548 #[test]
4549 fn test_async_output_requires_a_spare_execution_slot() {
4550 let error =
4551 match CopperListsManager::<Msgs, 1>::new(Some(Box::new(RecordingWriter::default()))) {
4552 Ok(_) => panic!("async output unexpectedly accepted a single CopperList slot"),
4553 Err(error) => error,
4554 };
4555
4556 assert!(error.to_string().contains("at least two CopperList slots"));
4557 }
4558
4559 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4560 #[test]
4561 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
4562 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
4563
4564 {
4565 let culist = copperlists.create().unwrap();
4566 assert_eq!(culist.id, 0);
4567 assert_eq!(culist.get_state(), CopperListState::Initialized);
4568 culist.msgs.0 = 41;
4569 culist.change_state(CopperListState::Processing);
4570 }
4571
4572 copperlists.end_of_processing(0).unwrap();
4573 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4574
4575 let reused = copperlists.create().unwrap();
4576 assert_eq!(reused.id, 1);
4577 assert_eq!(reused.get_state(), CopperListState::Initialized);
4578 assert_eq!(reused.msgs.0, 41);
4579 }
4580
4581 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
4582 #[test]
4583 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
4584 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
4585 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
4586
4587 let culist = copperlists.create().unwrap();
4588 assert_eq!(culist.id, 0);
4589 assert_eq!(culist.get_state(), CopperListState::Initialized);
4590
4591 let _ = copperlists.end_of_processing(0);
4592 }
4593
4594 #[test]
4595 fn test_runtime_task_input_order() {
4596 let mut config = CuConfig::default();
4597 let graph = config.get_graph_mut(None).unwrap();
4598 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
4599 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
4600 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
4601
4602 assert_eq!(src1_id, 0);
4603 assert_eq!(src2_id, 1);
4604
4605 let src1_type = "src1_type";
4607 let src2_type = "src2_type";
4608 graph.connect(src2_id, sink_id, src2_type).unwrap();
4609 graph.connect(src1_id, sink_id, src1_type).unwrap();
4610
4611 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
4612 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
4613 assert_eq!(src1_edge_id, 1);
4616 assert_eq!(src2_edge_id, 0);
4617
4618 let runtime = compute_runtime_plan(graph).unwrap();
4619 let sink_step = runtime
4620 .steps
4621 .iter()
4622 .find_map(|step| match step {
4623 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
4624 _ => None,
4625 })
4626 .unwrap();
4627
4628 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
4631 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
4632 }
4633
4634 #[test]
4635 fn test_runtime_output_ports_unique_ordered() {
4636 let mut config = CuConfig::default();
4637 let graph = config.get_graph_mut(None).unwrap();
4638 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
4639 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
4640 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
4641 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
4642 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
4643
4644 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
4645 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
4646 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
4647 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
4648
4649 let runtime = compute_runtime_plan(graph).unwrap();
4650 let src_step = runtime
4651 .steps
4652 .iter()
4653 .find_map(|step| match step {
4654 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4655 _ => None,
4656 })
4657 .unwrap();
4658
4659 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4660 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
4661
4662 let dst_a_step = runtime
4663 .steps
4664 .iter()
4665 .find_map(|step| match step {
4666 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
4667 _ => None,
4668 })
4669 .unwrap();
4670 let dst_b_step = runtime
4671 .steps
4672 .iter()
4673 .find_map(|step| match step {
4674 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
4675 _ => None,
4676 })
4677 .unwrap();
4678 let dst_a2_step = runtime
4679 .steps
4680 .iter()
4681 .find_map(|step| match step {
4682 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
4683 _ => None,
4684 })
4685 .unwrap();
4686 let dst_c_step = runtime
4687 .steps
4688 .iter()
4689 .find_map(|step| match step {
4690 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
4691 _ => None,
4692 })
4693 .unwrap();
4694
4695 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
4696 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
4697 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
4698 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
4699 }
4700
4701 #[test]
4702 fn test_runtime_plan_distinguishes_channel_distinct_outputs() {
4703 let mut config = CuConfig::default();
4704 let graph = config.get_graph_mut(None).unwrap();
4705 let src_id = graph.add_node(Node::new("cam", "Cam")).unwrap();
4706 let sink_id = graph.add_node(Node::new("sink", "Sink")).unwrap();
4707
4708 graph
4712 .connect_ext(
4713 src_id,
4714 sink_id,
4715 "msg::Image",
4716 None,
4717 Some("left".to_string()),
4718 None,
4719 )
4720 .unwrap();
4721 graph
4722 .connect_ext(
4723 src_id,
4724 sink_id,
4725 "msg::Image",
4726 None,
4727 Some("right".to_string()),
4728 None,
4729 )
4730 .unwrap();
4731
4732 let runtime = compute_runtime_plan(graph).unwrap();
4733
4734 let src_step = runtime
4735 .steps
4736 .iter()
4737 .find_map(|step| match step {
4738 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4739 _ => None,
4740 })
4741 .unwrap();
4742 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4743 assert_eq!(output_pack.msg_types, vec!["msg::Image", "msg::Image"]);
4744 assert_eq!(
4745 output_pack.src_channels,
4746 vec![Some("left".to_string()), Some("right".to_string())]
4747 );
4748
4749 let sink_step = runtime
4750 .steps
4751 .iter()
4752 .find_map(|step| match step {
4753 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
4754 _ => None,
4755 })
4756 .unwrap();
4757
4758 assert_eq!(sink_step.input_msg_indices_types.len(), 2);
4759 let ports: Vec<usize> = sink_step
4760 .input_msg_indices_types
4761 .iter()
4762 .map(|input| input.src_port)
4763 .collect();
4764 assert_eq!(ports, vec![0, 1]);
4765 }
4766
4767 #[test]
4768 fn test_runtime_output_ports_fanout_single() {
4769 let mut config = CuConfig::default();
4770 let graph = config.get_graph_mut(None).unwrap();
4771 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
4772 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
4773 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
4774
4775 graph.connect(src_id, dst_a_id, "i32").unwrap();
4776 graph.connect(src_id, dst_b_id, "i32").unwrap();
4777
4778 let runtime = compute_runtime_plan(graph).unwrap();
4779 let src_step = runtime
4780 .steps
4781 .iter()
4782 .find_map(|step| match step {
4783 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4784 _ => None,
4785 })
4786 .unwrap();
4787
4788 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4789 assert_eq!(output_pack.msg_types, vec!["i32"]);
4790 }
4791
4792 #[test]
4793 fn test_runtime_output_ports_include_nc_outputs() {
4794 let mut config = CuConfig::default();
4795 let graph = config.get_graph_mut(None).unwrap();
4796 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
4797 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
4798 graph.connect(src_id, dst_id, "msg::A").unwrap();
4799 graph
4800 .get_node_mut(src_id)
4801 .expect("missing source node")
4802 .add_nc_output("msg::B", usize::MAX);
4803
4804 let runtime = compute_runtime_plan(graph).unwrap();
4805 let src_step = runtime
4806 .steps
4807 .iter()
4808 .find_map(|step| match step {
4809 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4810 _ => None,
4811 })
4812 .unwrap();
4813 let dst_step = runtime
4814 .steps
4815 .iter()
4816 .find_map(|step| match step {
4817 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4818 _ => None,
4819 })
4820 .unwrap();
4821
4822 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4823 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
4824 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
4825 }
4826
4827 #[test]
4828 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
4829 let txt = r#"(
4830 tasks: [
4831 (id: "src", type: "a"),
4832 (id: "regular", type: "b"),
4833 ],
4834 cnx: [
4835 (src: "src", dst: "regular", msg: "msg::A"),
4836 (src: "regular", dst: "__nc__", msg: "msg::B"),
4837 ]
4838 )"#;
4839 let config = CuConfig::deserialize_ron(txt).unwrap();
4840 let graph = config.get_graph(None).unwrap();
4841 let regular_id = graph.get_node_id_by_name("regular").unwrap();
4842
4843 let runtime = compute_runtime_plan(graph).unwrap();
4844 let regular_step = runtime
4845 .steps
4846 .iter()
4847 .find_map(|step| match step {
4848 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
4849 _ => None,
4850 })
4851 .unwrap();
4852
4853 assert_eq!(regular_step.task_type, CuTaskType::Regular);
4854 assert_eq!(
4855 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
4856 vec!["msg::B"]
4857 );
4858 }
4859
4860 #[test]
4861 fn test_runtime_output_ports_respect_connection_order_with_nc() {
4862 let txt = r#"(
4863 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4864 cnx: [
4865 (src: "src", dst: "__nc__", msg: "msg::A"),
4866 (src: "src", dst: "sink", msg: "msg::B"),
4867 ]
4868 )"#;
4869 let config = CuConfig::deserialize_ron(txt).unwrap();
4870 let graph = config.get_graph(None).unwrap();
4871 let src_id = graph.get_node_id_by_name("src").unwrap();
4872 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4873
4874 let runtime = compute_runtime_plan(graph).unwrap();
4875 let src_step = runtime
4876 .steps
4877 .iter()
4878 .find_map(|step| match step {
4879 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4880 _ => None,
4881 })
4882 .unwrap();
4883 let dst_step = runtime
4884 .steps
4885 .iter()
4886 .find_map(|step| match step {
4887 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4888 _ => None,
4889 })
4890 .unwrap();
4891
4892 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4893 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
4894 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4895 }
4896
4897 #[cfg(feature = "std")]
4898 #[test]
4899 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
4900 let txt = r#"(
4901 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4902 cnx: [
4903 (src: "src", dst: "__nc__", msg: "msg::A"),
4904 (src: "src", dst: "sink", msg: "msg::B"),
4905 ]
4906 )"#;
4907 let tmp = tempfile::NamedTempFile::new().unwrap();
4908 std::fs::write(tmp.path(), txt).unwrap();
4909 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
4910 let graph = config.get_graph(None).unwrap();
4911 let src_id = graph.get_node_id_by_name("src").unwrap();
4912 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4913
4914 let runtime = compute_runtime_plan(graph).unwrap();
4915 let src_step = runtime
4916 .steps
4917 .iter()
4918 .find_map(|step| match step {
4919 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4920 _ => None,
4921 })
4922 .unwrap();
4923 let dst_step = runtime
4924 .steps
4925 .iter()
4926 .find_map(|step| match step {
4927 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4928 _ => None,
4929 })
4930 .unwrap();
4931
4932 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4933 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
4934 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4935 }
4936
4937 #[test]
4938 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
4939 let txt = r#"(
4940 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4941 cnx: [
4942 (src: "src", dst: "__nc__", msg: "i32"),
4943 (src: "src", dst: "sink", msg: "bool"),
4944 ]
4945 )"#;
4946 let config = CuConfig::deserialize_ron(txt).unwrap();
4947 let graph = config.get_graph(None).unwrap();
4948 let src_id = graph.get_node_id_by_name("src").unwrap();
4949 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4950
4951 let runtime = compute_runtime_plan(graph).unwrap();
4952 let src_step = runtime
4953 .steps
4954 .iter()
4955 .find_map(|step| match step {
4956 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4957 _ => None,
4958 })
4959 .unwrap();
4960 let dst_step = runtime
4961 .steps
4962 .iter()
4963 .find_map(|step| match step {
4964 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4965 _ => None,
4966 })
4967 .unwrap();
4968
4969 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4970 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
4971 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4972 }
4973
4974 #[test]
4975 fn test_runtime_plan_diamond_case1() {
4976 let mut config = CuConfig::default();
4978 let graph = config.get_graph_mut(None).unwrap();
4979 let cam0_id = graph
4980 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
4981 .unwrap();
4982 let inf0_id = graph
4983 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
4984 .unwrap();
4985 let broadcast_id = graph
4986 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
4987 .unwrap();
4988
4989 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
4991 graph.connect(cam0_id, inf0_id, "i32").unwrap();
4992 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
4993
4994 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
4995 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
4996
4997 assert_eq!(edge_cam0_to_inf0, 0);
4998 assert_eq!(edge_cam0_to_broadcast, 1);
4999
5000 let runtime = compute_runtime_plan(graph).unwrap();
5001 let broadcast_step = runtime
5002 .steps
5003 .iter()
5004 .find_map(|step| match step {
5005 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
5006 _ => None,
5007 })
5008 .unwrap();
5009
5010 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
5011 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
5012 }
5013
5014 #[test]
5015 fn test_runtime_plan_diamond_case2() {
5016 let mut config = CuConfig::default();
5018 let graph = config.get_graph_mut(None).unwrap();
5019 let cam0_id = graph
5020 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
5021 .unwrap();
5022 let inf0_id = graph
5023 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
5024 .unwrap();
5025 let broadcast_id = graph
5026 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
5027 .unwrap();
5028
5029 graph.connect(cam0_id, inf0_id, "i32").unwrap();
5031 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
5032 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
5033
5034 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
5035 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
5036
5037 assert_eq!(edge_cam0_to_broadcast, 0);
5038 assert_eq!(edge_cam0_to_inf0, 1);
5039
5040 let runtime = compute_runtime_plan(graph).unwrap();
5041 let broadcast_step = runtime
5042 .steps
5043 .iter()
5044 .find_map(|step| match step {
5045 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
5046 _ => None,
5047 })
5048 .unwrap();
5049
5050 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
5051 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
5052 }
5053
5054 use crate::config::AnytimeConfig;
5057
5058 fn anytime_node(id: &str, max_refines: Option<u32>) -> Node {
5059 let mut node = Node::new(id, "tasks::AnytimeTask");
5060 node.set_anytime(Some(AnytimeConfig {
5061 max_refines,
5062 ..Default::default()
5063 }));
5064 node
5065 }
5066
5067 fn plan_shape(plan: &CuExecutionLoop) -> Vec<(NodeId, CuStepPhase)> {
5069 plan.steps
5070 .iter()
5071 .map(|unit| match unit {
5072 CuExecutionUnit::Step(step) => (step.node_id, step.phase),
5073 CuExecutionUnit::Loop(_) => panic!("no loops expected"),
5074 })
5075 .collect()
5076 }
5077
5078 fn manual_step(node: Node, node_id: NodeId, inputs: &[u32], output: u32) -> CuExecutionUnit {
5081 CuExecutionUnit::Step(Box::new(CuExecutionStep {
5082 node_id,
5083 node,
5084 task_type: CuTaskType::Regular,
5085 phase: CuStepPhase::default(),
5086 input_msg_indices_types: inputs
5087 .iter()
5088 .map(|&culist_index| CuInputMsg {
5089 culist_index,
5090 msg_type: "msg::A".to_string(),
5091 src_port: 0,
5092 edge_id: 0,
5093 connection_order: 0,
5094 })
5095 .collect(),
5096 output_msg_pack: Some(CuOutputPack {
5097 culist_index: output,
5098 msg_types: vec!["msg::A".to_string()],
5099 src_channels: vec![None],
5100 }),
5101 }))
5102 }
5103
5104 #[test]
5105 fn test_anytime_expansion_contiguous_without_gap() {
5106 let mut config = CuConfig::default();
5109 let graph = config.get_graph_mut(None).unwrap();
5110 let src_id = graph.add_node(Node::new("src", "tasks::Src")).unwrap();
5111 let any_id = graph.add_node(anytime_node("any", Some(3))).unwrap();
5112 let sink_id = graph.add_node(Node::new("sink", "tasks::Sink")).unwrap();
5113 graph.connect(src_id, any_id, "msg::A").unwrap();
5114 graph.connect(any_id, sink_id, "msg::B").unwrap();
5115
5116 let mut plan = compute_runtime_plan(graph).unwrap();
5117 expand_anytime_steps(&mut plan).unwrap();
5118
5119 assert_eq!(
5120 plan_shape(&plan),
5121 vec![
5122 (src_id, CuStepPhase::Whole),
5123 (any_id, CuStepPhase::AnytimeBase),
5124 (any_id, CuStepPhase::AnytimeRefine),
5125 (any_id, CuStepPhase::AnytimeRefine),
5126 (any_id, CuStepPhase::AnytimeRefine),
5127 (sink_id, CuStepPhase::Whole),
5128 ]
5129 );
5130
5131 let (base_pack, refine_steps): (Option<CuOutputPack>, Vec<&CuExecutionStep>) = {
5133 let mut base_pack = None;
5134 let mut refines = Vec::new();
5135 for unit in &plan.steps {
5136 if let CuExecutionUnit::Step(step) = unit {
5137 match step.phase {
5138 CuStepPhase::AnytimeBase => base_pack = step.output_msg_pack.clone(),
5139 CuStepPhase::AnytimeRefine => refines.push(step.as_ref()),
5140 CuStepPhase::Whole => {}
5141 }
5142 }
5143 }
5144 (base_pack, refines)
5145 };
5146 let base_pack = base_pack.unwrap();
5147 for refine in refine_steps {
5148 assert!(refine.input_msg_indices_types.is_empty());
5149 let pack = refine.output_msg_pack.as_ref().unwrap();
5150 assert_eq!(pack.culist_index, base_pack.culist_index);
5151 }
5152 }
5153
5154 #[test]
5155 fn test_anytime_expansion_interleaves_with_gap_steps() {
5156 let mut plan = CuExecutionLoop {
5159 steps: vec![
5160 manual_step(anytime_node("any", Some(4)), 0, &[], 0),
5161 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
5162 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
5163 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
5164 ],
5165 loop_count: None,
5166 };
5167 expand_anytime_steps(&mut plan).unwrap();
5168 assert_eq!(
5169 plan_shape(&plan),
5170 vec![
5171 (0, CuStepPhase::AnytimeBase),
5172 (0, CuStepPhase::AnytimeRefine),
5173 (1, CuStepPhase::Whole),
5174 (0, CuStepPhase::AnytimeRefine),
5175 (2, CuStepPhase::Whole),
5176 (0, CuStepPhase::AnytimeRefine),
5177 (0, CuStepPhase::AnytimeRefine),
5178 (3, CuStepPhase::Whole),
5179 ]
5180 );
5181 }
5182
5183 #[test]
5184 fn test_anytime_expansion_fewer_refines_than_gaps() {
5185 let mut plan = CuExecutionLoop {
5187 steps: vec![
5188 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
5189 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
5190 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
5191 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
5192 ],
5193 loop_count: None,
5194 };
5195 expand_anytime_steps(&mut plan).unwrap();
5196 assert_eq!(
5197 plan_shape(&plan),
5198 vec![
5199 (0, CuStepPhase::AnytimeBase),
5200 (0, CuStepPhase::AnytimeRefine),
5201 (1, CuStepPhase::Whole),
5202 (0, CuStepPhase::AnytimeRefine),
5203 (2, CuStepPhase::Whole),
5204 (3, CuStepPhase::Whole),
5205 ]
5206 );
5207 }
5208
5209 #[test]
5210 fn test_anytime_expansion_without_consumer() {
5211 let mut plan = CuExecutionLoop {
5213 steps: vec![
5214 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
5215 manual_step(Node::new("other", "t"), 1, &[], 1),
5216 ],
5217 loop_count: None,
5218 };
5219 expand_anytime_steps(&mut plan).unwrap();
5220 assert_eq!(
5221 plan_shape(&plan),
5222 vec![
5223 (0, CuStepPhase::AnytimeBase),
5224 (0, CuStepPhase::AnytimeRefine),
5225 (0, CuStepPhase::AnytimeRefine),
5226 (1, CuStepPhase::Whole),
5227 ]
5228 );
5229 }
5230
5231 #[test]
5232 fn test_anytime_expansion_two_nodes_interleave() {
5233 let mut plan = CuExecutionLoop {
5236 steps: vec![
5237 manual_step(anytime_node("any_a", Some(2)), 0, &[], 0),
5238 manual_step(anytime_node("any_b", Some(2)), 1, &[], 1),
5239 manual_step(Node::new("consumer", "t"), 2, &[0, 1], 2),
5240 ],
5241 loop_count: None,
5242 };
5243 expand_anytime_steps(&mut plan).unwrap();
5244 assert_eq!(
5247 plan_shape(&plan),
5248 vec![
5249 (0, CuStepPhase::AnytimeBase),
5250 (0, CuStepPhase::AnytimeRefine),
5251 (1, CuStepPhase::AnytimeBase),
5252 (1, CuStepPhase::AnytimeRefine),
5253 (0, CuStepPhase::AnytimeRefine),
5254 (1, CuStepPhase::AnytimeRefine),
5255 (2, CuStepPhase::Whole),
5256 ]
5257 );
5258 }
5259
5260 #[test]
5261 fn test_anytime_expansion_requires_max_refines() {
5262 let mut plan = CuExecutionLoop {
5263 steps: vec![
5264 manual_step(anytime_node("any", None), 0, &[], 0),
5265 manual_step(Node::new("consumer", "t"), 1, &[0], 1),
5266 ],
5267 loop_count: None,
5268 };
5269 let err = expand_anytime_steps(&mut plan).unwrap_err();
5270 assert!(err.to_string().contains("needs anytime.max_refines"));
5271 }
5272}