1use crate::config::CuConfig;
5use crate::config::{
6 BridgeChannelConfigRepresentation, BridgeConfig, ComponentConfig, CuGraph, Flavor, NodeId,
7 TaskKind, resolve_task_kind_for_id,
8};
9use crate::context::CuContext;
10use crate::cutask::CuMsgMetadata;
11#[cfg(any(not(feature = "std"), not(target_has_atomic = "64")))]
12use crate::sync_compat::Mutex as SyncMutex;
13#[cfg(not(target_has_atomic = "64"))]
14use crate::sync_compat::MutexGuard as SyncMutexGuard;
15use bincode::Encode;
16use bincode::config::standard;
17use bincode::enc::EncoderImpl;
18use bincode::enc::write::SizeWriter;
19use compact_str::CompactString;
20use cu29_clock::CuDuration;
21#[allow(unused_imports)]
22use cu29_log::CuLogLevel;
23#[cfg(all(feature = "std", debug_assertions))]
24use cu29_log_runtime::{LiveLogListenerGuard, format_message_only, scoped_live_log_listener};
25use cu29_traits::{
26 CuError, CuResult, ObservedWriter, abort_observed_encode, begin_observed_encode,
27 finish_observed_encode,
28};
29use portable_atomic::{
30 AtomicBool as PortableAtomicBool, AtomicU64 as PortableAtomicU64, Ordering as PortableOrdering,
31};
32use serde_derive::{Deserialize, Serialize};
33
34#[cfg(not(feature = "std"))]
35extern crate alloc;
36
37#[cfg(feature = "std")]
38use core::cell::Cell;
39#[cfg(feature = "std")]
40use std::backtrace::Backtrace;
41#[cfg(feature = "std")]
42use std::fs::File;
43#[cfg(feature = "std")]
44use std::io::Write;
45#[cfg(feature = "std")]
46use std::panic::PanicHookInfo;
47#[cfg(feature = "std")]
48use std::sync::{Arc, Mutex as StdMutex, OnceLock};
49#[cfg(feature = "std")]
50use std::thread_local;
51#[cfg(feature = "std")]
52use std::time::{SystemTime, UNIX_EPOCH};
53#[cfg(feature = "std")]
54use std::{collections::HashMap as Map, string::String, string::ToString, vec::Vec};
55
56#[cfg(not(feature = "std"))]
57use alloc::{collections::BTreeMap as Map, string::String, string::ToString, vec::Vec};
58
59#[cfg(not(feature = "std"))]
60mod imp {
61 pub use alloc::alloc::{GlobalAlloc, Layout};
62 #[cfg(target_has_atomic = "64")]
63 pub use core::sync::atomic::AtomicU64;
64 pub use core::sync::atomic::{AtomicUsize, Ordering};
65 pub use libm::sqrt;
66}
67
68#[cfg(feature = "std")]
69mod imp {
70 #[cfg(feature = "memory_monitoring")]
71 use super::CountingAlloc;
72 #[cfg(feature = "memory_monitoring")]
73 pub use std::alloc::System;
74 pub use std::alloc::{GlobalAlloc, Layout};
75 #[cfg(target_has_atomic = "64")]
76 pub use std::sync::atomic::AtomicU64;
77 pub use std::sync::atomic::{AtomicUsize, Ordering};
78 #[cfg(feature = "memory_monitoring")]
79 #[global_allocator]
80 pub static GLOBAL: CountingAlloc<System> = CountingAlloc::new(System);
81}
82
83use imp::*;
84
85#[cfg(not(target_has_atomic = "64"))]
86fn lock_sync_mutex<T>(mutex: &SyncMutex<T>) -> SyncMutexGuard<'_, T> {
87 crate::sync_compat::lock(mutex)
88}
89
90#[cfg(all(feature = "std", debug_assertions))]
91fn format_timestamp(time: CuDuration) -> String {
92 let nanos = time.as_nanos();
94 let total_seconds = nanos / 1_000_000_000;
95 let hours = total_seconds / 3600;
96 let minutes = (total_seconds / 60) % 60;
97 let seconds = total_seconds % 60;
98 let fractional_1e4 = (nanos % 1_000_000_000) / 100_000;
99 format!("{hours:02}:{minutes:02}:{seconds:02}.{fractional_1e4:04}")
100}
101
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
104pub enum CuComponentState {
105 Start,
106 Preprocess,
107 Process,
108 Postprocess,
109 Stop,
110}
111
112#[repr(transparent)]
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
115pub struct ComponentId(usize);
116
117impl ComponentId {
118 pub const INVALID: Self = Self(usize::MAX);
119
120 #[inline]
121 pub const fn new(index: usize) -> Self {
122 Self(index)
123 }
124
125 #[inline]
126 pub const fn index(self) -> usize {
127 self.0
128 }
129}
130
131impl core::fmt::Display for ComponentId {
132 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
133 self.0.fmt(f)
134 }
135}
136
137impl From<ComponentId> for usize {
138 fn from(value: ComponentId) -> Self {
139 value.index()
140 }
141}
142
143#[repr(transparent)]
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146pub struct CuListSlot(usize);
147
148impl CuListSlot {
149 #[inline]
150 pub const fn new(index: usize) -> Self {
151 Self(index)
152 }
153
154 #[inline]
155 pub const fn index(self) -> usize {
156 self.0
157 }
158}
159
160impl From<CuListSlot> for usize {
161 fn from(value: CuListSlot) -> Self {
162 value.index()
163 }
164}
165
166#[derive(Debug, Clone, Copy)]
170pub struct CopperListLayout {
171 components: &'static [MonitorComponentMetadata],
172 slot_to_component: &'static [ComponentId],
173}
174
175impl CopperListLayout {
176 #[inline]
177 pub const fn new(
178 components: &'static [MonitorComponentMetadata],
179 slot_to_component: &'static [ComponentId],
180 ) -> Self {
181 Self {
182 components,
183 slot_to_component,
184 }
185 }
186
187 #[inline]
188 pub const fn components(self) -> &'static [MonitorComponentMetadata] {
189 self.components
190 }
191
192 #[inline]
193 pub const fn component_count(self) -> usize {
194 self.components.len()
195 }
196
197 #[inline]
198 pub const fn culist_slot_count(self) -> usize {
199 self.slot_to_component.len()
200 }
201
202 #[inline]
203 pub fn component(self, id: ComponentId) -> &'static MonitorComponentMetadata {
204 &self.components[id.index()]
205 }
206
207 #[inline]
208 pub fn component_for_slot(self, culist_slot: CuListSlot) -> ComponentId {
209 self.slot_to_component[culist_slot.index()]
210 }
211
212 #[inline]
213 pub const fn slot_to_component(self) -> &'static [ComponentId] {
214 self.slot_to_component
215 }
216
217 #[inline]
218 pub fn view<'a>(self, msgs: &'a [&'a CuMsgMetadata]) -> CopperListView<'a> {
219 CopperListView::new(self, msgs)
220 }
221}
222
223#[derive(Debug, Clone, Copy)]
225pub struct CopperListView<'a> {
226 layout: CopperListLayout,
227 msgs: &'a [&'a CuMsgMetadata],
228}
229
230impl<'a> CopperListView<'a> {
231 #[inline]
232 pub fn new(layout: CopperListLayout, msgs: &'a [&'a CuMsgMetadata]) -> Self {
233 assert_eq!(
234 msgs.len(),
235 layout.culist_slot_count(),
236 "invalid monitor CopperList view: msgs len {} != slot mapping len {}",
237 msgs.len(),
238 layout.culist_slot_count()
239 );
240 Self { layout, msgs }
241 }
242
243 #[inline]
244 pub const fn layout(self) -> CopperListLayout {
245 self.layout
246 }
247
248 #[inline]
249 pub const fn msgs(self) -> &'a [&'a CuMsgMetadata] {
250 self.msgs
251 }
252
253 #[inline]
254 pub const fn len(self) -> usize {
255 self.msgs.len()
256 }
257
258 #[inline]
259 pub const fn is_empty(self) -> bool {
260 self.msgs.is_empty()
261 }
262
263 #[inline]
264 pub fn entry(self, culist_slot: CuListSlot) -> CopperListEntry<'a> {
265 let index = culist_slot.index();
266 CopperListEntry {
267 culist_slot,
268 component_id: self.layout.component_for_slot(culist_slot),
269 msg: self.msgs[index],
270 }
271 }
272
273 pub fn entries(self) -> impl Iterator<Item = CopperListEntry<'a>> + 'a {
274 self.msgs.iter().enumerate().map(move |(idx, msg)| {
275 let culist_slot = CuListSlot::new(idx);
276 CopperListEntry {
277 culist_slot,
278 component_id: self.layout.component_for_slot(culist_slot),
279 msg,
280 }
281 })
282 }
283}
284
285#[derive(Debug, Clone, Copy)]
287pub struct CopperListEntry<'a> {
288 pub culist_slot: CuListSlot,
289 pub component_id: ComponentId,
290 pub msg: &'a CuMsgMetadata,
291}
292
293impl<'a> CopperListEntry<'a> {
294 #[inline]
295 pub fn component(self, layout: CopperListLayout) -> &'static MonitorComponentMetadata {
296 layout.component(self.component_id)
297 }
298
299 #[inline]
300 pub fn component_type(self, layout: CopperListLayout) -> ComponentType {
301 layout.component(self.component_id).kind()
302 }
303}
304
305#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
307pub struct ExecutionMarker {
308 pub component_id: ComponentId,
310 pub step: CuComponentState,
312 pub culistid: Option<u64>,
314}
315
316#[derive(Debug)]
322pub struct RuntimeExecutionProbe {
323 component_id: AtomicUsize,
324 step: AtomicUsize,
325 #[cfg(target_has_atomic = "64")]
326 culistid: AtomicU64,
327 #[cfg(target_has_atomic = "64")]
328 culistid_present: AtomicUsize,
329 #[cfg(not(target_has_atomic = "64"))]
330 culistid: SyncMutex<Option<u64>>,
331 sequence: AtomicUsize,
332}
333
334impl Default for RuntimeExecutionProbe {
335 fn default() -> Self {
336 Self {
337 component_id: AtomicUsize::new(ComponentId::INVALID.index()),
338 step: AtomicUsize::new(0),
339 #[cfg(target_has_atomic = "64")]
340 culistid: AtomicU64::new(0),
341 #[cfg(target_has_atomic = "64")]
342 culistid_present: AtomicUsize::new(0),
343 #[cfg(not(target_has_atomic = "64"))]
344 culistid: SyncMutex::new(None),
345 sequence: AtomicUsize::new(0),
346 }
347 }
348}
349
350impl RuntimeExecutionProbe {
351 #[inline]
352 pub fn record(&self, marker: ExecutionMarker) {
353 self.component_id
354 .store(marker.component_id.index(), Ordering::Relaxed);
355 self.step
356 .store(component_state_to_usize(marker.step), Ordering::Relaxed);
357 #[cfg(target_has_atomic = "64")]
358 match marker.culistid {
359 Some(culistid) => {
360 self.culistid.store(culistid, Ordering::Relaxed);
361 self.culistid_present.store(1, Ordering::Relaxed);
362 }
363 None => {
364 self.culistid_present.store(0, Ordering::Relaxed);
365 }
366 }
367 #[cfg(not(target_has_atomic = "64"))]
368 {
369 *lock_sync_mutex(&self.culistid) = marker.culistid;
370 }
371 self.sequence.fetch_add(1, Ordering::Release);
372 }
373
374 #[inline]
375 pub fn sequence(&self) -> usize {
376 self.sequence.load(Ordering::Acquire)
377 }
378
379 #[inline]
380 pub fn marker(&self) -> Option<ExecutionMarker> {
381 loop {
384 let seq_before = self.sequence.load(Ordering::Acquire);
385 let component_id = self.component_id.load(Ordering::Relaxed);
386 let step = self.step.load(Ordering::Relaxed);
387 #[cfg(target_has_atomic = "64")]
388 let culistid_present = self.culistid_present.load(Ordering::Relaxed);
389 #[cfg(target_has_atomic = "64")]
390 let culistid_value = self.culistid.load(Ordering::Relaxed);
391 #[cfg(not(target_has_atomic = "64"))]
392 let culistid = *lock_sync_mutex(&self.culistid);
393 let seq_after = self.sequence.load(Ordering::Acquire);
394 if seq_before == seq_after {
395 if component_id == ComponentId::INVALID.index() {
396 return None;
397 }
398 let step = usize_to_component_state(step);
399 #[cfg(target_has_atomic = "64")]
400 let culistid = if culistid_present == 0 {
401 None
402 } else {
403 Some(culistid_value)
404 };
405 return Some(ExecutionMarker {
406 component_id: ComponentId::new(component_id),
407 step,
408 culistid,
409 });
410 }
411 }
412 }
413}
414
415#[inline]
416const fn component_state_to_usize(step: CuComponentState) -> usize {
417 match step {
418 CuComponentState::Start => 0,
419 CuComponentState::Preprocess => 1,
420 CuComponentState::Process => 2,
421 CuComponentState::Postprocess => 3,
422 CuComponentState::Stop => 4,
423 }
424}
425
426#[inline]
427const fn usize_to_component_state(step: usize) -> CuComponentState {
428 match step {
429 0 => CuComponentState::Start,
430 1 => CuComponentState::Preprocess,
431 2 => CuComponentState::Process,
432 3 => CuComponentState::Postprocess,
433 _ => CuComponentState::Stop,
434 }
435}
436
437#[cfg(feature = "std")]
438pub type ExecutionProbeHandle = Arc<RuntimeExecutionProbe>;
439
440#[derive(Debug, Clone)]
445pub struct MonitorExecutionProbe {
446 #[cfg(feature = "std")]
447 inner: Option<ExecutionProbeHandle>,
448}
449
450impl Default for MonitorExecutionProbe {
451 fn default() -> Self {
452 Self::unavailable()
453 }
454}
455
456impl MonitorExecutionProbe {
457 #[cfg(feature = "std")]
458 pub fn from_shared(handle: ExecutionProbeHandle) -> Self {
459 Self {
460 inner: Some(handle),
461 }
462 }
463
464 pub const fn unavailable() -> Self {
465 Self {
466 #[cfg(feature = "std")]
467 inner: None,
468 }
469 }
470
471 pub fn is_available(&self) -> bool {
472 #[cfg(feature = "std")]
473 {
474 self.inner.is_some()
475 }
476 #[cfg(not(feature = "std"))]
477 {
478 false
479 }
480 }
481
482 pub fn marker(&self) -> Option<ExecutionMarker> {
483 #[cfg(feature = "std")]
484 {
485 self.inner.as_ref().and_then(|probe| probe.marker())
486 }
487 #[cfg(not(feature = "std"))]
488 {
489 None
490 }
491 }
492
493 pub fn sequence(&self) -> Option<usize> {
494 #[cfg(feature = "std")]
495 {
496 self.inner.as_ref().map(|probe| probe.sequence())
497 }
498 #[cfg(not(feature = "std"))]
499 {
500 None
501 }
502 }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
510#[non_exhaustive]
511pub enum ComponentType {
512 Source,
513 Task,
514 Sink,
515 Bridge,
516}
517
518impl ComponentType {
519 pub const fn is_task(self) -> bool {
520 !matches!(self, Self::Bridge)
521 }
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub struct MonitorComponentMetadata {
527 id: &'static str,
528 kind: ComponentType,
529 type_name: Option<&'static str>,
530}
531
532impl MonitorComponentMetadata {
533 pub const fn new(
534 id: &'static str,
535 kind: ComponentType,
536 type_name: Option<&'static str>,
537 ) -> Self {
538 Self {
539 id,
540 kind,
541 type_name,
542 }
543 }
544
545 pub const fn id(&self) -> &'static str {
547 self.id
548 }
549
550 pub const fn kind(&self) -> ComponentType {
551 self.kind
552 }
553
554 pub const fn type_name(&self) -> Option<&'static str> {
556 self.type_name
557 }
558}
559
560#[derive(Debug, Clone)]
565pub struct CuMonitoringMetadata {
566 mission_id: CompactString,
567 subsystem_id: Option<CompactString>,
568 instance_id: u32,
569 layout: CopperListLayout,
570 copperlist_info: CopperListInfo,
571 topology: MonitorTopology,
572 monitor_config: Option<ComponentConfig>,
573}
574
575impl CuMonitoringMetadata {
576 pub fn new(
577 mission_id: CompactString,
578 components: &'static [MonitorComponentMetadata],
579 culist_component_mapping: &'static [ComponentId],
580 copperlist_info: CopperListInfo,
581 topology: MonitorTopology,
582 monitor_config: Option<ComponentConfig>,
583 ) -> CuResult<Self> {
584 Self::validate_components(components)?;
585 Self::validate_culist_mapping(components.len(), culist_component_mapping)?;
586 Ok(Self {
587 mission_id,
588 subsystem_id: None,
589 instance_id: 0,
590 layout: CopperListLayout::new(components, culist_component_mapping),
591 copperlist_info,
592 topology,
593 monitor_config,
594 })
595 }
596
597 fn validate_components(components: &'static [MonitorComponentMetadata]) -> CuResult<()> {
598 let mut seen_bridge = false;
599 for component in components {
600 match component.kind() {
601 component_type if component_type.is_task() && seen_bridge => {
602 return Err(CuError::from(
603 "invalid monitor metadata: task-family components must appear before bridges",
604 ));
605 }
606 ComponentType::Bridge => seen_bridge = true,
607 _ => {}
608 }
609 }
610 Ok(())
611 }
612
613 fn validate_culist_mapping(
614 components_len: usize,
615 culist_component_mapping: &'static [ComponentId],
616 ) -> CuResult<()> {
617 for component_idx in culist_component_mapping {
618 if component_idx.index() >= components_len {
619 return Err(CuError::from(
620 "invalid monitor metadata: culist mapping points past components table",
621 ));
622 }
623 }
624 Ok(())
625 }
626
627 pub fn mission_id(&self) -> &str {
629 self.mission_id.as_str()
630 }
631
632 pub fn subsystem_id(&self) -> Option<&str> {
635 self.subsystem_id.as_deref()
636 }
637
638 pub fn instance_id(&self) -> u32 {
640 self.instance_id
641 }
642
643 pub fn components(&self) -> &'static [MonitorComponentMetadata] {
647 self.layout.components()
648 }
649
650 pub const fn component_count(&self) -> usize {
652 self.layout.component_count()
653 }
654
655 pub const fn layout(&self) -> CopperListLayout {
657 self.layout
658 }
659
660 pub fn component(&self, component_id: ComponentId) -> &'static MonitorComponentMetadata {
661 self.layout.component(component_id)
662 }
663
664 pub fn component_id(&self, component_id: ComponentId) -> &'static str {
665 self.component(component_id).id()
666 }
667
668 pub fn component_kind(&self, component_id: ComponentId) -> ComponentType {
669 self.component(component_id).kind()
670 }
671
672 pub fn component_index_by_id(&self, component_id: &str) -> Option<ComponentId> {
673 self.layout
674 .components()
675 .iter()
676 .position(|component| component.id() == component_id)
677 .map(ComponentId::new)
678 }
679
680 pub fn culist_component_mapping(&self) -> &'static [ComponentId] {
684 self.layout.slot_to_component()
685 }
686
687 pub fn component_for_culist_slot(&self, culist_slot: CuListSlot) -> ComponentId {
688 self.layout.component_for_slot(culist_slot)
689 }
690
691 pub fn copperlist_view<'a>(&self, msgs: &'a [&'a CuMsgMetadata]) -> CopperListView<'a> {
692 self.layout.view(msgs)
693 }
694
695 pub const fn copperlist_info(&self) -> CopperListInfo {
696 self.copperlist_info
697 }
698
699 pub fn topology(&self) -> &MonitorTopology {
704 &self.topology
705 }
706
707 pub fn monitor_config(&self) -> Option<&ComponentConfig> {
708 self.monitor_config.as_ref()
709 }
710
711 pub fn with_monitor_config(mut self, monitor_config: Option<ComponentConfig>) -> Self {
712 self.monitor_config = monitor_config;
713 self
714 }
715
716 pub fn with_subsystem_id(mut self, subsystem_id: Option<&str>) -> Self {
717 self.subsystem_id = subsystem_id.map(CompactString::from);
718 self
719 }
720
721 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
722 self.instance_id = instance_id;
723 self
724 }
725}
726
727#[derive(Debug, Clone, Default)]
731pub struct CuMonitoringRuntime {
732 execution_probe: MonitorExecutionProbe,
733}
734
735impl CuMonitoringRuntime {
736 #[cfg(feature = "std")]
737 pub fn new(execution_probe: MonitorExecutionProbe) -> Self {
738 ensure_runtime_panic_hook_installed();
739 Self { execution_probe }
740 }
741
742 #[cfg(not(feature = "std"))]
743 pub const fn new(execution_probe: MonitorExecutionProbe) -> Self {
744 Self { execution_probe }
745 }
746
747 #[cfg(feature = "std")]
748 pub fn unavailable() -> Self {
749 Self::new(MonitorExecutionProbe::unavailable())
750 }
751
752 #[cfg(not(feature = "std"))]
753 pub const fn unavailable() -> Self {
754 Self::new(MonitorExecutionProbe::unavailable())
755 }
756
757 pub fn execution_probe(&self) -> &MonitorExecutionProbe {
758 &self.execution_probe
759 }
760
761 #[cfg(feature = "std")]
762 pub fn register_panic_cleanup<F>(&self, callback: F) -> PanicHookRegistration
763 where
764 F: Fn(&PanicReport) + Send + Sync + 'static,
765 {
766 ensure_runtime_panic_hook_installed();
767 register_panic_cleanup(callback)
768 }
769
770 #[cfg(feature = "std")]
771 pub fn register_panic_action<F>(&self, callback: F) -> PanicHookRegistration
772 where
773 F: Fn(&PanicReport) -> Option<i32> + Send + Sync + 'static,
774 {
775 ensure_runtime_panic_hook_installed();
776 register_panic_action(callback)
777 }
778}
779
780#[cfg(feature = "std")]
781type PanicCleanupCallback = Arc<dyn Fn(&PanicReport) + Send + Sync + 'static>;
782#[cfg(feature = "std")]
783type PanicActionCallback = Arc<dyn Fn(&PanicReport) -> Option<i32> + Send + Sync + 'static>;
784
785#[cfg(feature = "std")]
786#[derive(Debug, Clone)]
787pub struct PanicReport {
788 message: String,
789 location: Option<String>,
790 thread_name: Option<String>,
791 backtrace: String,
792 timestamp_unix_ms: u128,
793 crash_report_path: Option<String>,
794}
795
796#[cfg(feature = "std")]
797impl PanicReport {
798 fn capture(info: &PanicHookInfo<'_>) -> Self {
799 let location = info
800 .location()
801 .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
802 let thread_name = std::thread::current().name().map(|name| name.to_string());
803 let timestamp_unix_ms = SystemTime::now()
804 .duration_since(UNIX_EPOCH)
805 .map(|dur| dur.as_millis())
806 .unwrap_or(0);
807
808 Self {
809 message: panic_hook_payload_to_string(info),
810 location,
811 thread_name,
812 backtrace: Backtrace::force_capture().to_string(),
813 timestamp_unix_ms,
814 crash_report_path: None,
815 }
816 }
817
818 pub fn message(&self) -> &str {
819 &self.message
820 }
821
822 pub fn location(&self) -> Option<&str> {
823 self.location.as_deref()
824 }
825
826 pub fn thread_name(&self) -> Option<&str> {
827 self.thread_name.as_deref()
828 }
829
830 pub fn backtrace(&self) -> &str {
831 &self.backtrace
832 }
833
834 pub fn timestamp_unix_ms(&self) -> u128 {
835 self.timestamp_unix_ms
836 }
837
838 pub fn crash_report_path(&self) -> Option<&str> {
839 self.crash_report_path.as_deref()
840 }
841
842 pub fn summary(&self) -> String {
843 match self.location() {
844 Some(location) => format!("panic at {location}: {}", self.message()),
845 None => format!("panic: {}", self.message()),
846 }
847 }
848}
849
850#[cfg(feature = "std")]
851#[derive(Clone, Copy, Debug, PartialEq, Eq)]
852enum PanicHookRegistrationKind {
853 Cleanup,
854 Action,
855}
856
857#[cfg(feature = "std")]
858#[derive(Clone)]
859struct RegisteredPanicCleanup {
860 id: usize,
861 callback: PanicCleanupCallback,
862}
863
864#[cfg(feature = "std")]
865#[derive(Clone)]
866struct RegisteredPanicAction {
867 id: usize,
868 callback: PanicActionCallback,
869}
870
871#[cfg(feature = "std")]
872#[derive(Default)]
873struct PanicHookRegistry {
874 cleanup_callbacks: StdMutex<Vec<RegisteredPanicCleanup>>,
875 action_callbacks: StdMutex<Vec<RegisteredPanicAction>>,
876}
877
878#[cfg(feature = "std")]
879#[derive(Debug)]
880pub struct PanicHookRegistration {
881 id: usize,
882 kind: PanicHookRegistrationKind,
883}
884
885#[cfg(feature = "std")]
886impl Drop for PanicHookRegistration {
887 fn drop(&mut self) {
888 unregister_panic_hook(self.kind, self.id);
889 }
890}
891
892#[cfg(feature = "std")]
893static PANIC_HOOK_REGISTRY: OnceLock<PanicHookRegistry> = OnceLock::new();
894#[cfg(feature = "std")]
895static PANIC_HOOK_INSTALL_ONCE: OnceLock<()> = OnceLock::new();
896#[cfg(feature = "std")]
897static PANIC_HOOK_REGISTRATION_ID: AtomicUsize = AtomicUsize::new(1);
898#[cfg(feature = "std")]
899static PANIC_HOOK_ACTIVE_COUNT: AtomicUsize = AtomicUsize::new(0);
900
901#[cfg(feature = "std")]
902fn panic_hook_registry() -> &'static PanicHookRegistry {
903 PANIC_HOOK_REGISTRY.get_or_init(PanicHookRegistry::default)
904}
905
906#[cfg(feature = "std")]
907fn ensure_runtime_panic_hook_installed() {
908 let _ = PANIC_HOOK_INSTALL_ONCE.get_or_init(|| {
909 std::panic::set_hook(Box::new(move |info| {
910 let _guard = PanicHookActiveGuard::new();
911 let mut report = PanicReport::capture(info);
912 run_panic_cleanup_callbacks(&report);
913 report.crash_report_path = write_panic_report_to_file(&report);
914 emit_panic_report(&report);
915
916 if let Some(exit_code) = run_panic_action_callbacks(&report) {
917 std::process::exit(exit_code);
918 }
919 }));
920 });
921}
922
923#[cfg(feature = "std")]
924struct PanicHookActiveGuard;
925
926#[cfg(feature = "std")]
927impl PanicHookActiveGuard {
928 fn new() -> Self {
929 PANIC_HOOK_ACTIVE_COUNT.fetch_add(1, Ordering::SeqCst);
930 Self
931 }
932}
933
934#[cfg(feature = "std")]
935impl Drop for PanicHookActiveGuard {
936 fn drop(&mut self) {
937 PANIC_HOOK_ACTIVE_COUNT.fetch_sub(1, Ordering::SeqCst);
938 }
939}
940
941#[cfg(feature = "std")]
942pub fn runtime_panic_hook_active() -> bool {
943 PANIC_HOOK_ACTIVE_COUNT.load(Ordering::SeqCst) > 0
944}
945
946#[cfg(not(feature = "std"))]
947pub const fn runtime_panic_hook_active() -> bool {
948 false
949}
950
951#[cfg(feature = "std")]
952fn register_panic_cleanup<F>(callback: F) -> PanicHookRegistration
953where
954 F: Fn(&PanicReport) + Send + Sync + 'static,
955{
956 let id = PANIC_HOOK_REGISTRATION_ID.fetch_add(1, Ordering::Relaxed);
957 let callback = Arc::new(callback) as PanicCleanupCallback;
958 let mut callbacks = panic_hook_registry()
959 .cleanup_callbacks
960 .lock()
961 .unwrap_or_else(|poison| poison.into_inner());
962 callbacks.push(RegisteredPanicCleanup { id, callback });
963 PanicHookRegistration {
964 id,
965 kind: PanicHookRegistrationKind::Cleanup,
966 }
967}
968
969#[cfg(feature = "std")]
970fn register_panic_action<F>(callback: F) -> PanicHookRegistration
971where
972 F: Fn(&PanicReport) -> Option<i32> + Send + Sync + 'static,
973{
974 let id = PANIC_HOOK_REGISTRATION_ID.fetch_add(1, Ordering::Relaxed);
975 let callback = Arc::new(callback) as PanicActionCallback;
976 let mut callbacks = panic_hook_registry()
977 .action_callbacks
978 .lock()
979 .unwrap_or_else(|poison| poison.into_inner());
980 callbacks.push(RegisteredPanicAction { id, callback });
981 PanicHookRegistration {
982 id,
983 kind: PanicHookRegistrationKind::Action,
984 }
985}
986
987#[cfg(feature = "std")]
988fn unregister_panic_hook(kind: PanicHookRegistrationKind, id: usize) {
989 let registry = panic_hook_registry();
990 match kind {
991 PanicHookRegistrationKind::Cleanup => {
992 let mut callbacks = registry
993 .cleanup_callbacks
994 .lock()
995 .unwrap_or_else(|poison| poison.into_inner());
996 callbacks.retain(|entry| entry.id != id);
997 }
998 PanicHookRegistrationKind::Action => {
999 let mut callbacks = registry
1000 .action_callbacks
1001 .lock()
1002 .unwrap_or_else(|poison| poison.into_inner());
1003 callbacks.retain(|entry| entry.id != id);
1004 }
1005 }
1006}
1007
1008#[cfg(feature = "std")]
1009fn run_panic_cleanup_callbacks(report: &PanicReport) {
1010 let callbacks = panic_hook_registry()
1011 .cleanup_callbacks
1012 .lock()
1013 .unwrap_or_else(|poison| poison.into_inner())
1014 .clone();
1015 for entry in callbacks {
1016 (entry.callback)(report);
1017 }
1018}
1019
1020#[cfg(feature = "std")]
1021fn run_panic_action_callbacks(report: &PanicReport) -> Option<i32> {
1022 let callbacks = panic_hook_registry()
1023 .action_callbacks
1024 .lock()
1025 .unwrap_or_else(|poison| poison.into_inner())
1026 .clone();
1027 let mut exit_code = None;
1028 for entry in callbacks {
1029 if exit_code.is_none() {
1030 exit_code = (entry.callback)(report);
1031 } else {
1032 let _ = (entry.callback)(report);
1033 }
1034 }
1035 exit_code
1036}
1037
1038#[cfg(feature = "std")]
1039fn panic_hook_payload_to_string(info: &PanicHookInfo<'_>) -> String {
1040 if let Some(msg) = info.payload().downcast_ref::<&str>() {
1041 (*msg).to_string()
1042 } else if let Some(msg) = info.payload().downcast_ref::<String>() {
1043 msg.clone()
1044 } else {
1045 "panic with non-string payload".to_string()
1046 }
1047}
1048
1049#[cfg(feature = "std")]
1050fn render_panic_report(report: &PanicReport) -> String {
1051 let mut rendered = String::from("Copper panic\n");
1052 rendered.push_str(&format!("time_unix_ms: {}\n", report.timestamp_unix_ms()));
1053 rendered.push_str(&format!(
1054 "thread: {}\n",
1055 report.thread_name().unwrap_or("<unnamed>")
1056 ));
1057 if let Some(location) = report.location() {
1058 rendered.push_str(&format!("location: {location}\n"));
1059 }
1060 rendered.push_str(&format!("message: {}\n", report.message()));
1061 if let Some(path) = report.crash_report_path() {
1062 rendered.push_str(&format!("crash_report: {path}\n"));
1063 }
1064 rendered.push_str("\nBacktrace:\n");
1065 rendered.push_str(report.backtrace());
1066 if !report.backtrace().ends_with('\n') {
1067 rendered.push('\n');
1068 }
1069 rendered
1070}
1071
1072#[cfg(feature = "std")]
1073fn emit_panic_report(report: &PanicReport) {
1074 let mut stderr = std::io::stderr().lock();
1075 let _ = stderr.write_all(render_panic_report(report).as_bytes());
1076 let _ = stderr.flush();
1077}
1078
1079#[cfg(feature = "std")]
1080fn write_panic_report_to_file(report: &PanicReport) -> Option<String> {
1081 let cwd = std::env::current_dir().ok()?;
1082 let file_name = format!(
1083 "copper-crash-{}-{}.txt",
1084 report.timestamp_unix_ms(),
1085 std::process::id()
1086 );
1087 let path = cwd.join(file_name);
1088 let path_string = path.to_string_lossy().to_string();
1089 let mut file = File::create(&path).ok()?;
1090 let mut report_with_path = report.clone();
1091 report_with_path.crash_report_path = Some(path_string.clone());
1092 file.write_all(render_panic_report(&report_with_path).as_bytes())
1093 .ok()?;
1094 file.flush().ok()?;
1095 Some(path_string)
1096}
1097
1098#[derive(Debug)]
1100pub enum Decision {
1101 Abort, Ignore, Shutdown, }
1105
1106fn merge_decision(lhs: Decision, rhs: Decision) -> Decision {
1107 use Decision::{Abort, Ignore, Shutdown};
1108 match (lhs, rhs) {
1111 (Shutdown, _) | (_, Shutdown) => Shutdown,
1112 (Abort, _) | (_, Abort) => Abort,
1113 _ => Ignore,
1114 }
1115}
1116
1117#[derive(Debug, Clone)]
1118pub struct MonitorNode {
1119 pub id: String,
1120 pub type_name: Option<String>,
1121 pub kind: ComponentType,
1122 pub inputs: Vec<String>,
1124 pub outputs: Vec<String>,
1126}
1127
1128#[derive(Debug, Clone)]
1129pub struct MonitorConnection {
1130 pub src: String,
1131 pub src_port: Option<String>,
1132 pub dst: String,
1133 pub dst_port: Option<String>,
1134 pub msg: String,
1135}
1136
1137#[derive(Debug, Clone, Default)]
1138pub struct MonitorTopology {
1139 pub nodes: Vec<MonitorNode>,
1140 pub connections: Vec<MonitorConnection>,
1141}
1142
1143#[derive(Debug, Clone, Copy, Default)]
1144pub struct CopperListInfo {
1145 pub size_bytes: usize,
1146 pub count: usize,
1147}
1148
1149impl CopperListInfo {
1150 pub const fn new(size_bytes: usize, count: usize) -> Self {
1151 Self { size_bytes, count }
1152 }
1153}
1154
1155#[derive(Debug, Clone, Copy, Default)]
1157pub struct CopperListIoStats {
1158 pub raw_culist_bytes: u64,
1163 pub handle_bytes: u64,
1169 pub encoded_culist_bytes: u64,
1171 pub keyframe_bytes: u64,
1173 pub structured_log_bytes_total: u64,
1175 pub culistid: u64,
1177}
1178
1179#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1180pub struct PayloadIoStats {
1181 pub resident_bytes: usize,
1182 pub encoded_bytes: usize,
1183 pub handle_bytes: usize,
1184}
1185
1186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1187pub struct CuMsgIoStats {
1188 pub present: bool,
1189 pub resident_bytes: u64,
1190 pub encoded_bytes: u64,
1191 pub handle_bytes: u64,
1192}
1193
1194struct CuMsgIoEntry {
1195 present: PortableAtomicBool,
1196 resident_bytes: PortableAtomicU64,
1197 encoded_bytes: PortableAtomicU64,
1198 handle_bytes: PortableAtomicU64,
1199}
1200
1201impl CuMsgIoEntry {
1202 fn clear(&self) {
1203 self.present.store(false, PortableOrdering::Release);
1204 self.resident_bytes.store(0, PortableOrdering::Relaxed);
1205 self.encoded_bytes.store(0, PortableOrdering::Relaxed);
1206 self.handle_bytes.store(0, PortableOrdering::Relaxed);
1207 }
1208
1209 fn get(&self) -> CuMsgIoStats {
1210 if !self.present.load(PortableOrdering::Acquire) {
1211 return CuMsgIoStats::default();
1212 }
1213
1214 CuMsgIoStats {
1215 present: true,
1216 resident_bytes: self.resident_bytes.load(PortableOrdering::Relaxed),
1217 encoded_bytes: self.encoded_bytes.load(PortableOrdering::Relaxed),
1218 handle_bytes: self.handle_bytes.load(PortableOrdering::Relaxed),
1219 }
1220 }
1221
1222 fn set(&self, stats: CuMsgIoStats) {
1223 self.resident_bytes
1224 .store(stats.resident_bytes, PortableOrdering::Relaxed);
1225 self.encoded_bytes
1226 .store(stats.encoded_bytes, PortableOrdering::Relaxed);
1227 self.handle_bytes
1228 .store(stats.handle_bytes, PortableOrdering::Relaxed);
1229 self.present.store(stats.present, PortableOrdering::Release);
1230 }
1231}
1232
1233impl Default for CuMsgIoEntry {
1234 fn default() -> Self {
1235 Self {
1236 present: PortableAtomicBool::new(false),
1237 resident_bytes: PortableAtomicU64::new(0),
1238 encoded_bytes: PortableAtomicU64::new(0),
1239 handle_bytes: PortableAtomicU64::new(0),
1240 }
1241 }
1242}
1243
1244pub struct CuMsgIoCache<const N: usize> {
1245 entries: [CuMsgIoEntry; N],
1246}
1247
1248impl<const N: usize> CuMsgIoCache<N> {
1249 pub fn clear(&self) {
1250 for entry in &self.entries {
1251 entry.clear();
1252 }
1253 }
1254
1255 pub fn get(&self, idx: usize) -> CuMsgIoStats {
1256 self.entries[idx].get()
1257 }
1258
1259 fn raw_parts(&self) -> (usize, usize) {
1260 (self.entries.as_ptr() as usize, N)
1261 }
1262}
1263
1264impl<const N: usize> Default for CuMsgIoCache<N> {
1265 fn default() -> Self {
1266 Self {
1267 entries: core::array::from_fn(|_| CuMsgIoEntry::default()),
1268 }
1269 }
1270}
1271
1272#[derive(Clone, Copy)]
1273struct ActiveCuMsgIoCapture {
1274 cache_addr: usize,
1275 cache_len: usize,
1276 current_slot: Option<usize>,
1277}
1278
1279#[cfg(feature = "std")]
1280thread_local! {
1281 static PAYLOAD_HANDLE_BYTES: Cell<Option<usize>> = const { Cell::new(None) };
1282 static ACTIVE_COPPERLIST_CAPTURE: Cell<Option<ActiveCuMsgIoCapture>> = const { Cell::new(None) };
1283 static LAST_COMPLETED_HANDLE_BYTES: Cell<u64> = const { Cell::new(0) };
1284}
1285
1286#[cfg(not(feature = "std"))]
1287static PAYLOAD_HANDLE_BYTES: SyncMutex<Option<usize>> = SyncMutex::new(None);
1288#[cfg(not(feature = "std"))]
1289static ACTIVE_COPPERLIST_CAPTURE: SyncMutex<Option<ActiveCuMsgIoCapture>> = SyncMutex::new(None);
1290#[cfg(not(feature = "std"))]
1291static LAST_COMPLETED_HANDLE_BYTES: SyncMutex<u64> = SyncMutex::new(0);
1292
1293fn begin_payload_io_measurement() {
1294 #[cfg(feature = "std")]
1295 PAYLOAD_HANDLE_BYTES.with(|bytes| {
1296 debug_assert!(
1297 bytes.get().is_none(),
1298 "payload IO byte measurement must not be nested"
1299 );
1300 bytes.set(Some(0));
1301 });
1302
1303 #[cfg(not(feature = "std"))]
1304 {
1305 let mut bytes = PAYLOAD_HANDLE_BYTES.lock();
1306 debug_assert!(
1307 bytes.is_none(),
1308 "payload IO byte measurement must not be nested"
1309 );
1310 *bytes = Some(0);
1311 }
1312}
1313
1314fn finish_payload_io_measurement() -> usize {
1315 #[cfg(feature = "std")]
1316 {
1317 PAYLOAD_HANDLE_BYTES.with(|bytes| bytes.replace(None).unwrap_or(0))
1318 }
1319
1320 #[cfg(not(feature = "std"))]
1321 {
1322 PAYLOAD_HANDLE_BYTES.lock().take().unwrap_or(0)
1323 }
1324}
1325
1326fn abort_payload_io_measurement() {
1327 #[cfg(feature = "std")]
1328 PAYLOAD_HANDLE_BYTES.with(|bytes| bytes.set(None));
1329
1330 #[cfg(not(feature = "std"))]
1331 {
1332 *PAYLOAD_HANDLE_BYTES.lock() = None;
1333 }
1334}
1335
1336fn current_payload_io_measurement() -> usize {
1337 #[cfg(feature = "std")]
1338 {
1339 PAYLOAD_HANDLE_BYTES.with(|bytes| bytes.get().unwrap_or(0))
1340 }
1341
1342 #[cfg(not(feature = "std"))]
1343 {
1344 PAYLOAD_HANDLE_BYTES.lock().as_ref().copied().unwrap_or(0)
1345 }
1346}
1347
1348pub(crate) fn record_payload_handle_bytes(bytes: usize) {
1353 #[cfg(feature = "std")]
1354 PAYLOAD_HANDLE_BYTES.with(|total| {
1355 if let Some(current) = total.get() {
1356 total.set(Some(current.saturating_add(bytes)));
1357 }
1358 });
1359
1360 #[cfg(not(feature = "std"))]
1361 {
1362 let mut total = PAYLOAD_HANDLE_BYTES.lock();
1363 if let Some(current) = *total {
1364 *total = Some(current.saturating_add(bytes));
1365 }
1366 }
1367}
1368
1369fn set_last_completed_handle_bytes(bytes: u64) {
1370 #[cfg(feature = "std")]
1371 LAST_COMPLETED_HANDLE_BYTES.with(|total| total.set(bytes));
1372
1373 #[cfg(not(feature = "std"))]
1374 {
1375 *LAST_COMPLETED_HANDLE_BYTES.lock() = bytes;
1376 }
1377}
1378
1379pub fn take_last_completed_handle_bytes() -> u64 {
1380 #[cfg(feature = "std")]
1381 {
1382 LAST_COMPLETED_HANDLE_BYTES.with(|total| total.replace(0))
1383 }
1384
1385 #[cfg(not(feature = "std"))]
1386 {
1387 let mut total = LAST_COMPLETED_HANDLE_BYTES.lock();
1388 let value = *total;
1389 *total = 0;
1390 value
1391 }
1392}
1393
1394fn with_active_capture_mut<R>(f: impl FnOnce(&mut ActiveCuMsgIoCapture) -> R) -> Option<R> {
1395 #[cfg(feature = "std")]
1396 {
1397 ACTIVE_COPPERLIST_CAPTURE.with(|capture| {
1398 let mut state = capture.get()?;
1399 let result = f(&mut state);
1400 capture.set(Some(state));
1401 Some(result)
1402 })
1403 }
1404
1405 #[cfg(not(feature = "std"))]
1406 {
1407 let mut capture = ACTIVE_COPPERLIST_CAPTURE.lock();
1408 let state = capture.as_mut()?;
1409 Some(f(state))
1410 }
1411}
1412
1413pub struct CuMsgIoCaptureGuard;
1414
1415impl CuMsgIoCaptureGuard {
1416 pub fn select_slot(&self, slot: usize) {
1417 let _ = with_active_capture_mut(|capture| {
1418 debug_assert!(slot < capture.cache_len, "payload IO slot out of range");
1419 capture.current_slot = Some(slot);
1420 });
1421 }
1422}
1423
1424impl Drop for CuMsgIoCaptureGuard {
1425 fn drop(&mut self) {
1426 set_last_completed_handle_bytes(finish_payload_io_measurement() as u64);
1427
1428 #[cfg(feature = "std")]
1429 ACTIVE_COPPERLIST_CAPTURE.with(|capture| capture.set(None));
1430
1431 #[cfg(not(feature = "std"))]
1432 {
1433 *ACTIVE_COPPERLIST_CAPTURE.lock() = None;
1434 }
1435 }
1436}
1437
1438pub fn start_copperlist_io_capture<const N: usize>(cache: &CuMsgIoCache<N>) -> CuMsgIoCaptureGuard {
1439 cache.clear();
1440 set_last_completed_handle_bytes(0);
1441 begin_payload_io_measurement();
1442 let (cache_addr, cache_len) = cache.raw_parts();
1443 let capture = ActiveCuMsgIoCapture {
1444 cache_addr,
1445 cache_len,
1446 current_slot: None,
1447 };
1448
1449 #[cfg(feature = "std")]
1450 ACTIVE_COPPERLIST_CAPTURE.with(|state| {
1451 debug_assert!(
1452 state.get().is_none(),
1453 "CopperList payload IO capture must not be nested"
1454 );
1455 state.set(Some(capture));
1456 });
1457
1458 #[cfg(not(feature = "std"))]
1459 {
1460 let mut state = ACTIVE_COPPERLIST_CAPTURE.lock();
1461 debug_assert!(
1462 state.is_none(),
1463 "CopperList payload IO capture must not be nested"
1464 );
1465 *state = Some(capture);
1466 }
1467
1468 CuMsgIoCaptureGuard
1469}
1470
1471pub(crate) fn current_payload_handle_bytes() -> usize {
1472 current_payload_io_measurement()
1473}
1474
1475pub(crate) fn record_current_slot_payload_io_stats(
1476 fixed_bytes: usize,
1477 encoded_bytes: usize,
1478 handle_bytes: usize,
1479) {
1480 let _ = with_active_capture_mut(|capture| {
1481 let Some(slot) = capture.current_slot else {
1482 return;
1483 };
1484 if slot >= capture.cache_len {
1485 return;
1486 }
1487 let cache_ptr = capture.cache_addr as *const CuMsgIoEntry;
1489 let entry = unsafe { &*cache_ptr.add(slot) };
1490 entry.set(CuMsgIoStats {
1491 present: true,
1492 resident_bytes: (fixed_bytes.saturating_add(handle_bytes)) as u64,
1493 encoded_bytes: encoded_bytes as u64,
1494 handle_bytes: handle_bytes as u64,
1495 });
1496 });
1497}
1498
1499pub fn payload_io_stats<T>(payload: &T) -> CuResult<PayloadIoStats>
1506where
1507 T: Encode,
1508{
1509 begin_payload_io_measurement();
1510 begin_observed_encode();
1511
1512 let result = (|| {
1513 let mut encoder =
1514 EncoderImpl::<_, _>::new(ObservedWriter::new(SizeWriter::default()), standard());
1515 payload.encode(&mut encoder).map_err(|e| {
1516 CuError::from("Failed to measure payload IO bytes").add_cause(&e.to_string())
1517 })?;
1518 let encoded_bytes = encoder.into_writer().into_inner().bytes_written;
1519 debug_assert_eq!(encoded_bytes, finish_observed_encode());
1520 let handle_bytes = finish_payload_io_measurement();
1521 Ok(PayloadIoStats {
1522 resident_bytes: core::mem::size_of::<T>().saturating_add(handle_bytes),
1523 encoded_bytes,
1524 handle_bytes,
1525 })
1526 })();
1527
1528 if result.is_err() {
1529 abort_payload_io_measurement();
1530 abort_observed_encode();
1531 }
1532
1533 result
1534}
1535
1536fn collect_output_ports(graph: &CuGraph, node_id: NodeId) -> Vec<(String, String)> {
1537 let Ok(msg_types) = graph.get_node_output_msg_types_by_id(node_id) else {
1538 return Vec::new();
1539 };
1540
1541 let mut outputs = Vec::new();
1542 for (port_idx, msg) in msg_types.into_iter().enumerate() {
1543 let mut port_label = String::from("out");
1544 port_label.push_str(&port_idx.to_string());
1545 port_label.push_str(": ");
1546 port_label.push_str(msg.as_str());
1547 outputs.push((msg, port_label));
1548 }
1549 outputs
1550}
1551
1552pub fn build_monitor_topology(config: &CuConfig, mission: &str) -> CuResult<MonitorTopology> {
1554 let graph = config.get_graph(Some(mission))?;
1555 let mut nodes: Map<String, MonitorNode> = Map::new();
1556 let mut output_port_lookup: Map<String, Map<String, String>> = Map::new();
1557
1558 let mut bridge_lookup: Map<&str, &BridgeConfig> = Map::new();
1559 for bridge in &config.bridges {
1560 bridge_lookup.insert(bridge.id.as_str(), bridge);
1561 }
1562
1563 for (node_idx, node) in graph.get_all_nodes() {
1564 let node_id = node.get_id();
1565 let task_kind = match node.get_flavor() {
1566 Flavor::Bridge => ComponentType::Bridge,
1567 Flavor::Task => match resolve_task_kind_for_id(graph, node_idx)? {
1568 TaskKind::Source => ComponentType::Source,
1569 TaskKind::Regular => ComponentType::Task,
1570 TaskKind::Sink => ComponentType::Sink,
1571 },
1572 };
1573
1574 let mut inputs = Vec::new();
1575 let mut outputs = Vec::new();
1576 if task_kind == ComponentType::Bridge
1577 && let Some(bridge) = bridge_lookup.get(node_id.as_str())
1578 {
1579 for ch in &bridge.channels {
1580 match ch {
1581 BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
1582 BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
1583 }
1584 }
1585 } else {
1586 match task_kind {
1587 ComponentType::Source => {
1588 let ports = collect_output_ports(graph, node_idx);
1589 let mut port_map: Map<String, String> = Map::new();
1590 for (msg_type, label) in ports {
1591 port_map.insert(msg_type, label.clone());
1592 outputs.push(label);
1593 }
1594 output_port_lookup.insert(node_id.clone(), port_map);
1595 }
1596 ComponentType::Task => {
1597 inputs.push("in".to_string());
1598 let ports = collect_output_ports(graph, node_idx);
1599 let mut port_map: Map<String, String> = Map::new();
1600 for (msg_type, label) in ports {
1601 port_map.insert(msg_type, label.clone());
1602 outputs.push(label);
1603 }
1604 output_port_lookup.insert(node_id.clone(), port_map);
1605 }
1606 ComponentType::Sink => {
1607 inputs.push("in".to_string());
1608 }
1609 ComponentType::Bridge => unreachable!("handled above"),
1610 }
1611 }
1612
1613 nodes.insert(
1614 node_id.clone(),
1615 MonitorNode {
1616 id: node_id,
1617 type_name: Some(node.get_type().to_string()),
1618 kind: task_kind,
1619 inputs,
1620 outputs,
1621 },
1622 );
1623 }
1624
1625 let mut connections = Vec::new();
1626 for cnx in graph.edges() {
1627 let src = cnx.src.clone();
1628 let dst = cnx.dst.clone();
1629
1630 let src_port = cnx.src_channel.clone().or_else(|| {
1631 output_port_lookup
1632 .get(&src)
1633 .and_then(|ports| ports.get(&cnx.msg).cloned())
1634 .or_else(|| {
1635 nodes
1636 .get(&src)
1637 .and_then(|node| node.outputs.first().cloned())
1638 })
1639 });
1640 let dst_port = cnx.dst_channel.clone().or_else(|| {
1641 nodes
1642 .get(&dst)
1643 .and_then(|node| node.inputs.first().cloned())
1644 });
1645
1646 connections.push(MonitorConnection {
1647 src,
1648 src_port,
1649 dst,
1650 dst_port,
1651 msg: cnx.msg.clone(),
1652 });
1653 }
1654
1655 Ok(MonitorTopology {
1656 nodes: nodes.into_values().collect(),
1657 connections,
1658 })
1659}
1660
1661pub trait CuMonitor: Sized {
1681 fn new(metadata: CuMonitoringMetadata, runtime: CuMonitoringRuntime) -> CuResult<Self>
1687 where
1688 Self: Sized;
1689
1690 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
1692 Ok(())
1693 }
1694
1695 fn process_copperlist(&self, _ctx: &CuContext, view: CopperListView<'_>) -> CuResult<()>;
1697
1698 fn observe_copperlist_io(&self, _stats: CopperListIoStats) {}
1700
1701 fn observe_alloc(
1713 &self,
1714 _component_id: ComponentId,
1715 _step: CuComponentState,
1716 _allocated_bytes: usize,
1717 _deallocated_bytes: usize,
1718 ) {
1719 }
1720
1721 fn process_error(
1725 &self,
1726 component_id: ComponentId,
1727 step: CuComponentState,
1728 error: &CuError,
1729 ) -> Decision;
1730
1731 fn process_panic(&self, _panic_message: &str) {}
1733
1734 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
1736 Ok(())
1737 }
1738}
1739
1740pub struct NoMonitor {
1743 #[cfg(all(feature = "std", debug_assertions))]
1744 live_log_listener: Option<LiveLogListenerGuard>,
1745}
1746impl CuMonitor for NoMonitor {
1747 fn new(_metadata: CuMonitoringMetadata, _runtime: CuMonitoringRuntime) -> CuResult<Self> {
1748 Ok(NoMonitor {
1749 #[cfg(all(feature = "std", debug_assertions))]
1750 live_log_listener: None,
1751 })
1752 }
1753
1754 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
1755 #[cfg(all(feature = "std", debug_assertions))]
1756 {
1757 self.live_log_listener = Some(scoped_live_log_listener(
1758 |entry, format_str, param_names| {
1759 let params: Vec<String> = entry.params.iter().map(|v| v.to_string()).collect();
1760 let named: Map<String, String> = param_names
1761 .iter()
1762 .zip(params.iter())
1763 .map(|(k, v)| (k.to_string(), v.clone()))
1764 .collect();
1765
1766 if let Ok(msg) = format_message_only(format_str, params.as_slice(), &named) {
1767 let ts = format_timestamp(entry.time.into());
1768 println!("{} [{:?}] {}", ts, entry.level, msg);
1769 }
1770 },
1771 ));
1772 }
1773 Ok(())
1774 }
1775
1776 fn process_copperlist(&self, _ctx: &CuContext, _view: CopperListView<'_>) -> CuResult<()> {
1777 Ok(())
1779 }
1780
1781 fn process_error(
1782 &self,
1783 _component_id: ComponentId,
1784 _step: CuComponentState,
1785 _error: &CuError,
1786 ) -> Decision {
1787 Decision::Ignore
1789 }
1790
1791 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
1792 #[cfg(all(feature = "std", debug_assertions))]
1793 {
1794 self.live_log_listener = None;
1795 }
1796 Ok(())
1797 }
1798}
1799
1800macro_rules! impl_monitor_tuple {
1801 ($($idx:tt => $name:ident),+) => {
1802 impl<$($name: CuMonitor),+> CuMonitor for ($($name,)+) {
1803 fn new(metadata: CuMonitoringMetadata, runtime: CuMonitoringRuntime) -> CuResult<Self>
1804 where
1805 Self: Sized,
1806 {
1807 Ok(($($name::new(metadata.clone(), runtime.clone())?,)+))
1808 }
1809
1810 fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
1811 $(self.$idx.start(ctx)?;)+
1812 Ok(())
1813 }
1814
1815 fn process_copperlist(&self, ctx: &CuContext, view: CopperListView<'_>) -> CuResult<()> {
1816 $(self.$idx.process_copperlist(ctx, view)?;)+
1817 Ok(())
1818 }
1819
1820 fn observe_copperlist_io(&self, stats: CopperListIoStats) {
1821 $(self.$idx.observe_copperlist_io(stats);)+
1822 }
1823
1824 fn observe_alloc(
1825 &self,
1826 component_id: ComponentId,
1827 step: CuComponentState,
1828 allocated_bytes: usize,
1829 deallocated_bytes: usize,
1830 ) {
1831 $(self.$idx.observe_alloc(component_id, step, allocated_bytes, deallocated_bytes);)+
1832 }
1833
1834 fn process_error(
1835 &self,
1836 component_id: ComponentId,
1837 step: CuComponentState,
1838 error: &CuError,
1839 ) -> Decision {
1840 let mut decision = Decision::Ignore;
1841 $(decision = merge_decision(decision, self.$idx.process_error(component_id, step, error));)+
1842 decision
1843 }
1844
1845 fn process_panic(&self, panic_message: &str) {
1846 $(self.$idx.process_panic(panic_message);)+
1847 }
1848
1849 fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
1850 $(self.$idx.stop(ctx)?;)+
1851 Ok(())
1852 }
1853 }
1854 };
1855}
1856
1857impl_monitor_tuple!(0 => M0, 1 => M1);
1858impl_monitor_tuple!(0 => M0, 1 => M1, 2 => M2);
1859impl_monitor_tuple!(0 => M0, 1 => M1, 2 => M2, 3 => M3);
1860impl_monitor_tuple!(0 => M0, 1 => M1, 2 => M2, 3 => M3, 4 => M4);
1861impl_monitor_tuple!(0 => M0, 1 => M1, 2 => M2, 3 => M3, 4 => M4, 5 => M5);
1862
1863#[cfg(feature = "std")]
1864pub fn panic_payload_to_string(payload: &(dyn core::any::Any + Send)) -> String {
1865 if let Some(msg) = payload.downcast_ref::<&str>() {
1866 (*msg).to_string()
1867 } else if let Some(msg) = payload.downcast_ref::<String>() {
1868 msg.clone()
1869 } else {
1870 "panic with non-string payload".to_string()
1871 }
1872}
1873
1874pub struct CountingAlloc<A: GlobalAlloc> {
1884 inner: A,
1885 allocated: AtomicUsize,
1886 deallocated: AtomicUsize,
1887}
1888
1889#[cfg(all(feature = "std", feature = "memory_monitoring"))]
1890thread_local! {
1891 static THREAD_ALLOCATED: Cell<usize> = const { Cell::new(0) };
1892 static THREAD_DEALLOCATED: Cell<usize> = const { Cell::new(0) };
1893}
1894
1895#[cfg(all(feature = "std", feature = "memory_monitoring"))]
1896#[inline]
1897fn bump_thread_allocated(bytes: usize) {
1898 let _ = THREAD_ALLOCATED.try_with(|c| c.set(c.get().wrapping_add(bytes)));
1902}
1903
1904#[cfg(all(feature = "std", feature = "memory_monitoring"))]
1905#[inline]
1906fn bump_thread_deallocated(bytes: usize) {
1907 let _ = THREAD_DEALLOCATED.try_with(|c| c.set(c.get().wrapping_add(bytes)));
1908}
1909
1910#[cfg(all(feature = "std", feature = "memory_monitoring"))]
1911#[inline]
1912fn read_thread_allocated() -> usize {
1913 THREAD_ALLOCATED.try_with(|c| c.get()).unwrap_or(0)
1914}
1915
1916#[cfg(all(feature = "std", feature = "memory_monitoring"))]
1917#[inline]
1918fn read_thread_deallocated() -> usize {
1919 THREAD_DEALLOCATED.try_with(|c| c.get()).unwrap_or(0)
1920}
1921
1922impl<A: GlobalAlloc> CountingAlloc<A> {
1923 pub const fn new(inner: A) -> Self {
1924 CountingAlloc {
1925 inner,
1926 allocated: AtomicUsize::new(0),
1927 deallocated: AtomicUsize::new(0),
1928 }
1929 }
1930
1931 pub fn allocated(&self) -> usize {
1932 self.allocated.load(Ordering::SeqCst)
1933 }
1934
1935 pub fn deallocated(&self) -> usize {
1936 self.deallocated.load(Ordering::SeqCst)
1937 }
1938
1939 pub fn reset(&self) {
1940 self.allocated.store(0, Ordering::SeqCst);
1941 self.deallocated.store(0, Ordering::SeqCst);
1942 }
1943}
1944
1945unsafe impl<A: GlobalAlloc> GlobalAlloc for CountingAlloc<A> {
1947 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1949 let p = unsafe { self.inner.alloc(layout) };
1951 if !p.is_null() {
1952 self.allocated.fetch_add(layout.size(), Ordering::SeqCst);
1953 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
1954 bump_thread_allocated(layout.size());
1955 }
1956 p
1957 }
1958
1959 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
1961 unsafe { self.inner.dealloc(ptr, layout) }
1963 self.deallocated.fetch_add(layout.size(), Ordering::SeqCst);
1964 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
1965 bump_thread_deallocated(layout.size());
1966 }
1967}
1968
1969#[inline]
1977pub fn global_allocated_bytes() -> Option<usize> {
1978 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
1979 {
1980 Some(GLOBAL.allocated())
1981 }
1982 #[cfg(not(all(feature = "std", feature = "memory_monitoring")))]
1983 {
1984 None
1985 }
1986}
1987
1988#[inline]
1992pub fn global_deallocated_bytes() -> Option<usize> {
1993 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
1994 {
1995 Some(GLOBAL.deallocated())
1996 }
1997 #[cfg(not(all(feature = "std", feature = "memory_monitoring")))]
1998 {
1999 None
2000 }
2001}
2002
2003#[must_use]
2033pub struct ScopedAllocCounter {
2034 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2035 bf_allocated: usize,
2036 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2037 bf_deallocated: usize,
2038 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2044 _not_send_sync: core::marker::PhantomData<*const ()>,
2045}
2046
2047impl Default for ScopedAllocCounter {
2048 fn default() -> Self {
2049 Self::new()
2050 }
2051}
2052
2053impl ScopedAllocCounter {
2054 #[inline]
2055 pub fn new() -> Self {
2056 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2057 {
2058 ScopedAllocCounter {
2059 bf_allocated: read_thread_allocated(),
2060 bf_deallocated: read_thread_deallocated(),
2061 _not_send_sync: core::marker::PhantomData,
2062 }
2063 }
2064 #[cfg(not(all(feature = "std", feature = "memory_monitoring")))]
2065 {
2066 ScopedAllocCounter {}
2067 }
2068 }
2069
2070 #[inline]
2073 pub fn allocated(&self) -> usize {
2074 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2075 {
2076 read_thread_allocated().wrapping_sub(self.bf_allocated)
2077 }
2078 #[cfg(not(all(feature = "std", feature = "memory_monitoring")))]
2079 {
2080 0
2081 }
2082 }
2083
2084 #[inline]
2087 pub fn deallocated(&self) -> usize {
2088 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2089 {
2090 read_thread_deallocated().wrapping_sub(self.bf_deallocated)
2091 }
2092 #[cfg(not(all(feature = "std", feature = "memory_monitoring")))]
2093 {
2094 0
2095 }
2096 }
2097}
2098
2099#[cfg(feature = "std")]
2100const BUCKET_COUNT: usize = 1024;
2101#[cfg(not(feature = "std"))]
2102const BUCKET_COUNT: usize = 256;
2103
2104#[derive(Debug, Clone)]
2107pub struct LiveStatistics {
2108 buckets: [u64; BUCKET_COUNT],
2109 min_val: u64,
2110 max_val: u64,
2111 sum: u128,
2112 sum_sq: u128,
2113 count: u64,
2114 max_value: u64,
2115}
2116
2117impl LiveStatistics {
2118 pub fn new_with_max(max_value: u64) -> Self {
2138 LiveStatistics {
2139 buckets: [0; BUCKET_COUNT],
2140 min_val: u64::MAX,
2141 max_val: 0,
2142 sum: 0,
2143 sum_sq: 0,
2144 count: 0,
2145 max_value,
2146 }
2147 }
2148
2149 #[inline]
2150 fn value_to_bucket(&self, value: u64) -> usize {
2151 if value >= self.max_value {
2152 BUCKET_COUNT - 1
2153 } else {
2154 ((value as u128 * BUCKET_COUNT as u128) / self.max_value as u128) as usize
2155 }
2156 }
2157
2158 #[inline]
2159 pub fn min(&self) -> u64 {
2160 if self.count == 0 { 0 } else { self.min_val }
2161 }
2162
2163 #[inline]
2164 pub fn max(&self) -> u64 {
2165 self.max_val
2166 }
2167
2168 #[inline]
2169 pub fn mean(&self) -> f64 {
2170 if self.count == 0 {
2171 0.0
2172 } else {
2173 self.sum as f64 / self.count as f64
2174 }
2175 }
2176
2177 #[inline]
2178 pub fn stdev(&self) -> f64 {
2179 if self.count == 0 {
2180 return 0.0;
2181 }
2182 let mean = self.mean();
2183 let variance = (self.sum_sq as f64 / self.count as f64) - (mean * mean);
2184 if variance < 0.0 {
2185 return 0.0;
2186 }
2187 #[cfg(feature = "std")]
2188 return variance.sqrt();
2189 #[cfg(not(feature = "std"))]
2190 return sqrt(variance);
2191 }
2192
2193 #[inline]
2194 pub fn percentile(&self, percentile: f64) -> u64 {
2195 if self.count == 0 {
2196 return 0;
2197 }
2198
2199 let target_count = (self.count as f64 * percentile) as u64;
2200 let mut accumulated = 0u64;
2201
2202 for (bucket_idx, &bucket_count) in self.buckets.iter().enumerate() {
2203 accumulated += bucket_count;
2204 if accumulated >= target_count {
2205 let bucket_start = (bucket_idx as u64 * self.max_value) / BUCKET_COUNT as u64;
2207 let bucket_end = ((bucket_idx + 1) as u64 * self.max_value) / BUCKET_COUNT as u64;
2208 let bucket_fraction = if bucket_count > 0 {
2209 (target_count - (accumulated - bucket_count)) as f64 / bucket_count as f64
2210 } else {
2211 0.5
2212 };
2213 return bucket_start
2214 + ((bucket_end - bucket_start) as f64 * bucket_fraction) as u64;
2215 }
2216 }
2217
2218 self.max_val
2219 }
2220
2221 #[inline]
2223 pub fn record(&mut self, value: u64) {
2224 if value < self.min_val {
2225 self.min_val = value;
2226 }
2227 if value > self.max_val {
2228 self.max_val = value;
2229 }
2230 let value_u128 = value as u128;
2231 self.sum += value_u128;
2232 self.sum_sq += value_u128 * value_u128;
2233 self.count += 1;
2234
2235 let bucket = self.value_to_bucket(value);
2236 self.buckets[bucket] += 1;
2237 }
2238
2239 #[inline]
2240 pub fn len(&self) -> u64 {
2241 self.count
2242 }
2243
2244 #[inline]
2245 pub fn is_empty(&self) -> bool {
2246 self.count == 0
2247 }
2248
2249 #[inline]
2250 pub fn reset(&mut self) {
2251 self.buckets.fill(0);
2252 self.min_val = u64::MAX;
2253 self.max_val = 0;
2254 self.sum = 0;
2255 self.sum_sq = 0;
2256 self.count = 0;
2257 }
2258}
2259
2260#[derive(Debug, Clone)]
2263pub struct CuDurationStatistics {
2264 bare: LiveStatistics,
2265 jitter: LiveStatistics,
2266 last_value: CuDuration,
2267}
2268
2269impl CuDurationStatistics {
2270 pub fn new(max: CuDuration) -> Self {
2271 let CuDuration(max) = max;
2272 CuDurationStatistics {
2273 bare: LiveStatistics::new_with_max(max),
2274 jitter: LiveStatistics::new_with_max(max),
2275 last_value: CuDuration::default(),
2276 }
2277 }
2278
2279 #[inline]
2280 pub fn min(&self) -> CuDuration {
2281 CuDuration(self.bare.min())
2282 }
2283
2284 #[inline]
2285 pub fn max(&self) -> CuDuration {
2286 CuDuration(self.bare.max())
2287 }
2288
2289 #[inline]
2290 pub fn mean(&self) -> CuDuration {
2291 CuDuration(self.bare.mean() as u64) }
2293
2294 #[inline]
2295 pub fn percentile(&self, percentile: f64) -> CuDuration {
2296 CuDuration(self.bare.percentile(percentile))
2297 }
2298
2299 #[inline]
2300 pub fn stddev(&self) -> CuDuration {
2301 CuDuration(self.bare.stdev() as u64)
2302 }
2303
2304 #[inline]
2305 pub fn len(&self) -> u64 {
2306 self.bare.len()
2307 }
2308
2309 #[inline]
2310 pub fn is_empty(&self) -> bool {
2311 self.bare.len() == 0
2312 }
2313
2314 #[inline]
2315 pub fn jitter_min(&self) -> CuDuration {
2316 CuDuration(self.jitter.min())
2317 }
2318
2319 #[inline]
2320 pub fn jitter_max(&self) -> CuDuration {
2321 CuDuration(self.jitter.max())
2322 }
2323
2324 #[inline]
2325 pub fn jitter_mean(&self) -> CuDuration {
2326 CuDuration(self.jitter.mean() as u64)
2327 }
2328
2329 #[inline]
2330 pub fn jitter_stddev(&self) -> CuDuration {
2331 CuDuration(self.jitter.stdev() as u64)
2332 }
2333
2334 #[inline]
2335 pub fn jitter_percentile(&self, percentile: f64) -> CuDuration {
2336 CuDuration(self.jitter.percentile(percentile))
2337 }
2338
2339 #[inline]
2340 pub fn record(&mut self, value: CuDuration) {
2341 let CuDuration(nanos) = value;
2342 if self.bare.is_empty() {
2343 self.bare.record(nanos);
2344 self.last_value = value;
2345 return;
2346 }
2347 self.bare.record(nanos);
2348 let CuDuration(last_nanos) = self.last_value;
2349 self.jitter.record(nanos.abs_diff(last_nanos));
2350 self.last_value = value;
2351 }
2352
2353 #[inline]
2354 pub fn reset(&mut self) {
2355 self.bare.reset();
2356 self.jitter.reset();
2357 }
2358}
2359
2360#[cfg(test)]
2361mod tests {
2362 use super::*;
2363 use core::sync::atomic::{AtomicUsize, Ordering};
2364
2365 #[derive(Clone, Copy)]
2366 enum TestDecision {
2367 Ignore,
2368 Abort,
2369 Shutdown,
2370 }
2371
2372 struct TestMonitor {
2373 decision: TestDecision,
2374 copperlist_calls: AtomicUsize,
2375 panic_calls: AtomicUsize,
2376 alloc_calls: AtomicUsize,
2377 last_alloc_bytes: AtomicUsize,
2378 last_dealloc_bytes: AtomicUsize,
2379 }
2380
2381 impl TestMonitor {
2382 fn new_with(decision: TestDecision) -> Self {
2383 Self {
2384 decision,
2385 copperlist_calls: AtomicUsize::new(0),
2386 panic_calls: AtomicUsize::new(0),
2387 alloc_calls: AtomicUsize::new(0),
2388 last_alloc_bytes: AtomicUsize::new(0),
2389 last_dealloc_bytes: AtomicUsize::new(0),
2390 }
2391 }
2392 }
2393
2394 fn test_metadata() -> CuMonitoringMetadata {
2395 const COMPONENTS: &[MonitorComponentMetadata] = &[
2396 MonitorComponentMetadata::new("a", ComponentType::Task, None),
2397 MonitorComponentMetadata::new("b", ComponentType::Task, None),
2398 ];
2399 CuMonitoringMetadata::new(
2400 CompactString::from(crate::config::DEFAULT_MISSION_ID),
2401 COMPONENTS,
2402 &[],
2403 CopperListInfo::new(0, 0),
2404 MonitorTopology::default(),
2405 None,
2406 )
2407 .expect("test metadata should be valid")
2408 }
2409
2410 impl CuMonitor for TestMonitor {
2411 fn new(_metadata: CuMonitoringMetadata, runtime: CuMonitoringRuntime) -> CuResult<Self> {
2412 let monitor = Self::new_with(TestDecision::Ignore);
2413 #[cfg(feature = "std")]
2414 let _ = runtime.execution_probe();
2415 Ok(monitor)
2416 }
2417
2418 fn process_copperlist(&self, _ctx: &CuContext, _view: CopperListView<'_>) -> CuResult<()> {
2419 self.copperlist_calls.fetch_add(1, Ordering::SeqCst);
2420 Ok(())
2421 }
2422
2423 fn process_error(
2424 &self,
2425 _component_id: ComponentId,
2426 _step: CuComponentState,
2427 _error: &CuError,
2428 ) -> Decision {
2429 match self.decision {
2430 TestDecision::Ignore => Decision::Ignore,
2431 TestDecision::Abort => Decision::Abort,
2432 TestDecision::Shutdown => Decision::Shutdown,
2433 }
2434 }
2435
2436 fn process_panic(&self, _panic_message: &str) {
2437 self.panic_calls.fetch_add(1, Ordering::SeqCst);
2438 }
2439
2440 fn observe_alloc(
2441 &self,
2442 _component_id: ComponentId,
2443 _step: CuComponentState,
2444 allocated_bytes: usize,
2445 deallocated_bytes: usize,
2446 ) {
2447 self.alloc_calls.fetch_add(1, Ordering::SeqCst);
2448 self.last_alloc_bytes
2449 .store(allocated_bytes, Ordering::SeqCst);
2450 self.last_dealloc_bytes
2451 .store(deallocated_bytes, Ordering::SeqCst);
2452 }
2453 }
2454
2455 #[test]
2456 fn test_live_statistics_percentiles() {
2457 let mut stats = LiveStatistics::new_with_max(1000);
2458
2459 for i in 0..100 {
2461 stats.record(i);
2462 }
2463
2464 assert_eq!(stats.len(), 100);
2465 assert_eq!(stats.min(), 0);
2466 assert_eq!(stats.max(), 99);
2467 assert_eq!(stats.mean() as u64, 49); let p50 = stats.percentile(0.5);
2471 let p90 = stats.percentile(0.90);
2472 let p95 = stats.percentile(0.95);
2473 let p99 = stats.percentile(0.99);
2474
2475 assert!((p50 as i64 - 49).abs() < 5, "p50={} expected ~49", p50);
2477 assert!((p90 as i64 - 89).abs() < 5, "p90={} expected ~89", p90);
2478 assert!((p95 as i64 - 94).abs() < 5, "p95={} expected ~94", p95);
2479 assert!((p99 as i64 - 98).abs() < 5, "p99={} expected ~98", p99);
2480 }
2481
2482 #[test]
2483 fn test_duration_stats() {
2484 let mut stats = CuDurationStatistics::new(CuDuration(1000));
2485 stats.record(CuDuration(100));
2486 stats.record(CuDuration(200));
2487 stats.record(CuDuration(500));
2488 stats.record(CuDuration(400));
2489 assert_eq!(stats.min(), CuDuration(100));
2490 assert_eq!(stats.max(), CuDuration(500));
2491 assert_eq!(stats.mean(), CuDuration(300));
2492 assert_eq!(stats.len(), 4);
2493 assert_eq!(stats.jitter.len(), 3);
2494 assert_eq!(stats.jitter_min(), CuDuration(100));
2495 assert_eq!(stats.jitter_max(), CuDuration(300));
2496 assert_eq!(stats.jitter_mean(), CuDuration((100 + 300 + 100) / 3));
2497 stats.reset();
2498 assert_eq!(stats.len(), 0);
2499 }
2500
2501 #[test]
2502 fn test_duration_stats_large_samples_do_not_overflow() {
2503 let mut stats = CuDurationStatistics::new(CuDuration(10_000_000_000));
2504 stats.record(CuDuration(5_000_000_000));
2505 stats.record(CuDuration(8_000_000_000));
2506
2507 assert_eq!(stats.min(), CuDuration(5_000_000_000));
2508 assert_eq!(stats.max(), CuDuration(8_000_000_000));
2509 assert_eq!(stats.mean(), CuDuration(6_500_000_000));
2510 assert!(stats.stddev().as_nanos().abs_diff(1_500_000_000) <= 1);
2511 assert_eq!(stats.jitter_mean(), CuDuration(3_000_000_000));
2512 }
2513
2514 #[test]
2515 fn tuple_monitor_merges_contradictory_decisions_with_strictest_wins() {
2516 let err = CuError::from("boom");
2517
2518 let two = (
2519 TestMonitor::new_with(TestDecision::Ignore),
2520 TestMonitor::new_with(TestDecision::Shutdown),
2521 );
2522 assert!(matches!(
2523 two.process_error(ComponentId::new(0), CuComponentState::Process, &err),
2524 Decision::Shutdown
2525 ));
2526
2527 let two = (
2528 TestMonitor::new_with(TestDecision::Ignore),
2529 TestMonitor::new_with(TestDecision::Abort),
2530 );
2531 assert!(matches!(
2532 two.process_error(ComponentId::new(0), CuComponentState::Process, &err),
2533 Decision::Abort
2534 ));
2535 }
2536
2537 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2538 #[test]
2539 fn scoped_alloc_counter_attributes_only_calling_threads_allocations() {
2540 use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
2545 use std::sync::{Arc, Barrier};
2546
2547 let barrier = Arc::new(Barrier::new(2));
2548 let other_running = Arc::new(AtomicBool::new(true));
2549
2550 let b = barrier.clone();
2551 let flag = other_running.clone();
2552 let other = std::thread::spawn(move || {
2553 b.wait();
2556 let mut total: usize = 0;
2557 while flag.load(AtomicOrdering::Relaxed) {
2558 let v: Vec<u8> = vec![0u8; 1024 * 16];
2559 total = total.wrapping_add(v.len());
2560 }
2561 total
2562 });
2563
2564 let counter = ScopedAllocCounter::new();
2565 barrier.wait();
2566 let local_alloc_size = 1024;
2569 let local = vec![0u8; local_alloc_size];
2570 std::thread::sleep(std::time::Duration::from_millis(50));
2571 let observed = counter.allocated();
2572 other_running.store(false, AtomicOrdering::Relaxed);
2573 drop(local);
2574 let _ = other.join();
2575
2576 assert!(
2580 observed >= local_alloc_size,
2581 "expected at least {local_alloc_size} B, got {observed}"
2582 );
2583 assert!(
2584 observed < 1024 * 1024,
2585 "thread-local scope leaked across threads: observed {observed} B"
2586 );
2587 }
2588
2589 #[test]
2590 fn scoped_alloc_counter_reports_zero_when_feature_off_and_nonzero_when_on() {
2591 let availability_matches = match global_allocated_bytes() {
2598 Some(_) => cfg!(all(feature = "std", feature = "memory_monitoring")),
2599 None => !cfg!(all(feature = "std", feature = "memory_monitoring")),
2600 };
2601 assert!(availability_matches, "feature-gate cfg mismatch");
2602
2603 let counter = ScopedAllocCounter::new();
2604 let _v = vec![0u8; 4096];
2605 let allocated = counter.allocated();
2606 if cfg!(all(feature = "std", feature = "memory_monitoring")) {
2607 assert!(
2608 allocated >= 4096,
2609 "expected at least 4096 B allocated, got {allocated}"
2610 );
2611 } else {
2612 assert_eq!(allocated, 0, "feature off must report 0");
2613 }
2614 }
2615
2616 #[test]
2617 fn tuple_monitor_fans_out_observe_alloc() {
2618 let monitors = <(TestMonitor, TestMonitor) as CuMonitor>::new(
2619 test_metadata(),
2620 CuMonitoringRuntime::unavailable(),
2621 )
2622 .expect("tuple new");
2623 monitors.observe_alloc(ComponentId::new(0), CuComponentState::Process, 128, 64);
2624
2625 assert_eq!(monitors.0.alloc_calls.load(Ordering::SeqCst), 1);
2626 assert_eq!(monitors.1.alloc_calls.load(Ordering::SeqCst), 1);
2627 assert_eq!(monitors.0.last_alloc_bytes.load(Ordering::SeqCst), 128);
2628 assert_eq!(monitors.1.last_dealloc_bytes.load(Ordering::SeqCst), 64);
2629 }
2630
2631 #[test]
2632 fn tuple_monitor_fans_out_callbacks() {
2633 let monitors = <(TestMonitor, TestMonitor) as CuMonitor>::new(
2634 test_metadata(),
2635 CuMonitoringRuntime::unavailable(),
2636 )
2637 .expect("tuple new");
2638 let (ctx, _clock_control) = CuContext::new_mock_clock();
2639 let empty_view = test_metadata().layout().view(&[]);
2640 monitors
2641 .process_copperlist(&ctx, empty_view)
2642 .expect("process_copperlist should fan out");
2643 monitors.process_panic("panic marker");
2644
2645 assert_eq!(monitors.0.copperlist_calls.load(Ordering::SeqCst), 1);
2646 assert_eq!(monitors.1.copperlist_calls.load(Ordering::SeqCst), 1);
2647 assert_eq!(monitors.0.panic_calls.load(Ordering::SeqCst), 1);
2648 assert_eq!(monitors.1.panic_calls.load(Ordering::SeqCst), 1);
2649 }
2650
2651 fn encoded_size<E: Encode>(value: &E) -> usize {
2652 let mut encoder = EncoderImpl::<_, _>::new(SizeWriter::default(), standard());
2653 value
2654 .encode(&mut encoder)
2655 .expect("size measurement encoder should not fail");
2656 encoder.into_writer().bytes_written
2657 }
2658
2659 #[test]
2660 fn payload_io_stats_tracks_encode_path_size_for_plain_payloads() {
2661 let payload = vec![1u8, 2, 3, 4];
2662 let io = payload_io_stats(&payload).expect("payload IO measurement should succeed");
2663
2664 assert_eq!(io.encoded_bytes, encoded_size(&payload));
2665 assert_eq!(io.resident_bytes, core::mem::size_of::<Vec<u8>>());
2666 assert_eq!(io.handle_bytes, 0);
2667 }
2668
2669 #[test]
2670 fn payload_io_stats_tracks_handle_backed_storage() {
2671 let payload = crate::pool::CuHandle::new_detached(vec![0u8; 32]);
2672 let io = payload_io_stats(&payload).expect("payload IO measurement should succeed");
2673
2674 assert_eq!(io.encoded_bytes, encoded_size(&payload));
2675 assert_eq!(
2676 io.resident_bytes,
2677 core::mem::size_of::<crate::pool::CuHandle<Vec<u8>>>() + 32
2678 );
2679 assert_eq!(io.handle_bytes, 32);
2680 }
2681
2682 #[test]
2683 fn runtime_execution_probe_roundtrip_marker() {
2684 let probe = RuntimeExecutionProbe::default();
2685 assert!(probe.marker().is_none());
2686 assert_eq!(probe.sequence(), 0);
2687
2688 probe.record(ExecutionMarker {
2689 component_id: ComponentId::new(7),
2690 step: CuComponentState::Process,
2691 culistid: Some(42),
2692 });
2693
2694 let marker = probe.marker().expect("marker should be available");
2695 assert_eq!(marker.component_id, ComponentId::new(7));
2696 assert!(matches!(marker.step, CuComponentState::Process));
2697 assert_eq!(marker.culistid, Some(42));
2698 assert_eq!(probe.sequence(), 1);
2699 }
2700}