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