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