1#![cfg_attr(not(feature = "std"), no_std)]
2
3extern crate alloc;
10
11use alloc::format;
12use alloc::string::String;
13use alloc::vec;
14use alloc::vec::Vec;
15use core::fmt::Write as _;
16use cu29::prelude::*;
17#[cfg(all(feature = "std", debug_assertions))]
18use cu29_log_runtime::{
19 format_message_only, register_live_log_listener, unregister_live_log_listener,
20};
21#[cfg(not(feature = "std"))]
22use spin::Mutex;
23#[cfg(not(feature = "std"))]
24type MutexGuard<'a, T> = spin::MutexGuard<'a, T, spin::relax::Spin>;
25#[cfg(all(feature = "std", debug_assertions))]
26use std::collections::HashMap;
27#[cfg(feature = "std")]
28use std::sync::{Mutex, MutexGuard};
29
30const REPORT_INTERVAL_SECS: u64 = 1;
31const MAX_LATENCY_SECS: u64 = 5;
32
33#[cfg(all(feature = "std", debug_assertions))]
34fn format_timestamp(time: CuTime) -> String {
35 let nanos = time.as_nanos();
37 let total_seconds = nanos / 1_000_000_000;
38 let hours = total_seconds / 3600;
39 let minutes = (total_seconds / 60) % 60;
40 let seconds = total_seconds % 60;
41 let fractional_1e4 = (nanos % 1_000_000_000) / 100_000; format!("{hours:02}:{minutes:02}:{seconds:02}.{fractional_1e4:04}")
43}
44
45struct WindowState {
46 total_copperlists: u64,
47 window_copperlists: u32,
48 last_report_at: Option<CuTime>,
49 last_log_duration: CuDuration,
50 end_to_end: CuDurationStatistics,
51 per_component: Vec<CuDurationStatistics>,
52}
53
54impl WindowState {
55 fn new(component_count: usize, max_sample: CuDuration) -> Self {
56 #[cfg(target_os = "none")]
57 info!("WindowState::new: init end_to_end");
58 let end_to_end = CuDurationStatistics::new(max_sample);
59 #[cfg(target_os = "none")]
60 info!("WindowState::new: init per_component");
61 #[cfg(target_os = "none")]
62 info!(
63 "WindowState::new: stats_size={} per_component_bytes={}",
64 core::mem::size_of::<CuDurationStatistics>(),
65 core::mem::size_of::<CuDurationStatistics>() * component_count
66 );
67 let per_component = vec![CuDurationStatistics::new(max_sample); component_count];
68 #[cfg(target_os = "none")]
69 info!("WindowState::new: init done");
70 Self {
71 total_copperlists: 0,
72 window_copperlists: 0,
73 last_report_at: None,
74 last_log_duration: CuDuration::MIN,
75 end_to_end,
76 per_component,
77 }
78 }
79
80 fn reset_window(&mut self, now: CuTime) {
81 self.window_copperlists = 0;
82 self.last_report_at = Some(now);
83 self.end_to_end.reset();
84 for stat in &mut self.per_component {
85 stat.reset();
86 }
87 }
88}
89
90fn lock_window(window: &Mutex<WindowState>) -> MutexGuard<'_, WindowState> {
91 #[cfg(feature = "std")]
92 {
93 window.lock().unwrap_or_else(|poison| poison.into_inner())
94 }
95
96 #[cfg(not(feature = "std"))]
97 {
98 window.lock()
99 }
100}
101
102fn monitor_max_sample(monitor_cfg: Option<&ComponentConfig>) -> CuResult<CuDuration> {
103 if let Some(cfg) = monitor_cfg {
104 if let Some(us) = cfg.get::<u64>("max_latency_us")? {
105 if us == 0 {
106 return Err(CuError::from("cu_logmon max_latency_us must be > 0"));
107 }
108 return Ok(CuDuration::from_micros(us));
109 }
110 if let Some(ms) = cfg.get::<u64>("max_latency_ms")? {
111 if ms == 0 {
112 return Err(CuError::from("cu_logmon max_latency_ms must be > 0"));
113 }
114 return Ok(CuDuration::from_millis(ms));
115 }
116 if let Some(secs) = cfg.get::<u64>("max_latency_secs")? {
117 if secs == 0 {
118 return Err(CuError::from("cu_logmon max_latency_secs must be > 0"));
119 }
120 return Ok(CuDuration::from_secs(secs));
121 }
122 }
123 Ok(CuDuration::from_secs(MAX_LATENCY_SECS))
124}
125
126struct Snapshot {
127 copperlist_index: u64,
128 rate_whole: u64,
129 rate_tenths: u64,
130 e2e_p50_us: u64,
131 e2e_p90_us: u64,
132 e2e_p99_us: u64,
133 e2e_max_us: u64,
134 top4: String,
135 overhead_us: u64,
136}
137
138pub struct CuLogMon {
139 components: &'static [MonitorComponentMetadata],
140 component_count: usize,
141 window: Mutex<WindowState>,
142}
143
144impl CuLogMon {
145 fn component_name(&self, component_id: ComponentId) -> &'static str {
146 debug_assert!(component_id.index() < self.component_count);
147 self.components[component_id.index()].id()
148 }
149
150 fn compute_snapshot(&self, state: &WindowState, now: CuTime) -> Option<Snapshot> {
151 let last_report = state.last_report_at?;
152
153 let elapsed = now - last_report;
154 let elapsed_ns = elapsed.as_nanos();
155
156 if elapsed_ns < CuDuration::from_secs(REPORT_INTERVAL_SECS).as_nanos() {
157 return None;
158 }
159
160 let rate_x10 = (state.window_copperlists as u64 * 10 * 1_000_000_000u64)
161 .checked_div(elapsed_ns)
162 .unwrap_or(0);
163
164 let top4_max_entries = find_top_components_by_max(&state.per_component, 4);
165 let mut top4 = String::new();
166 if top4_max_entries.is_empty() {
167 top4.push_str("none");
168 } else {
169 for (rank, (component_id, dur)) in top4_max_entries.iter().enumerate() {
170 if rank > 0 {
171 top4.push_str(", ");
172 }
173 let name = self.component_name(*component_id);
174 let _ = write!(&mut top4, "{} {}us", name, dur.as_micros());
175 }
176 }
177
178 let e2e_p50 = state.end_to_end.percentile(0.5).as_micros();
179 let e2e_p90 = state.end_to_end.percentile(0.9).as_micros();
180 let e2e_p99 = state.end_to_end.percentile(0.99).as_micros();
181 let e2e_max = state.end_to_end.max().as_micros().max(e2e_p99);
183
184 Some(Snapshot {
185 copperlist_index: state.total_copperlists,
186 rate_whole: rate_x10 / 10,
187 rate_tenths: rate_x10 % 10,
188 e2e_p50_us: e2e_p50,
189 e2e_p90_us: e2e_p90,
190 e2e_p99_us: e2e_p99,
191 e2e_max_us: e2e_max,
192 top4,
193 overhead_us: state.last_log_duration.as_micros(),
194 })
195 }
196}
197
198fn component_duration(meta: &CuMsgMetadata) -> Option<CuDuration> {
199 let start = Option::<CuTime>::from(meta.process_time.start)?;
200 let end = Option::<CuTime>::from(meta.process_time.end)?;
201 (end >= start).then_some(end - start)
202}
203
204fn end_to_end_latency(msgs: &[&CuMsgMetadata]) -> Option<CuDuration> {
205 let start = msgs
206 .first()
207 .and_then(|m| Option::<CuTime>::from(m.process_time.start));
208 let end = msgs
209 .last()
210 .and_then(|m| Option::<CuTime>::from(m.process_time.end));
211 match (start, end) {
212 (Some(s), Some(e)) if e >= s => Some(e - s),
213 _ => None,
214 }
215}
216
217fn find_top_components_by_max(
218 per_component: &[CuDurationStatistics],
219 limit: usize,
220) -> Vec<(ComponentId, CuDuration)> {
221 let mut ranked: Vec<(ComponentId, CuDuration)> = per_component
222 .iter()
223 .enumerate()
224 .filter_map(|(idx, stats)| {
225 (!stats.is_empty()).then_some((ComponentId::new(idx), stats.max()))
226 })
227 .collect();
228 ranked.sort_unstable_by(|a, b| {
229 b.1.as_nanos()
230 .cmp(&a.1.as_nanos())
231 .then_with(|| a.0.index().cmp(&b.0.index()))
232 });
233 ranked.truncate(limit);
234 ranked
235}
236
237fn component_state_label(state: &CuComponentState) -> &'static str {
238 match state {
239 CuComponentState::Start => "start",
240 CuComponentState::Preprocess => "pre",
241 CuComponentState::Process => "process",
242 CuComponentState::Postprocess => "post",
243 CuComponentState::Stop => "stop",
244 }
245}
246
247impl CuMonitor for CuLogMon {
248 fn new(metadata: CuMonitoringMetadata, _runtime: CuMonitoringRuntime) -> CuResult<Self> {
249 let components = metadata.components();
250 let component_count = components.len();
251 #[cfg(target_os = "none")]
252 info!("CuLogMon::new: component_count={}", component_count);
253 let max_sample = monitor_max_sample(metadata.monitor_config())?;
254 let window = WindowState::new(component_count, max_sample);
255 #[cfg(target_os = "none")]
256 info!("CuLogMon::new: window ready");
257 Ok(Self {
258 components,
259 component_count,
260 window: Mutex::new(window),
261 })
262 }
263
264 fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
265 let mut window = lock_window(&self.window);
266 window.last_report_at = Some(ctx.recent());
267 info!(
268 ctx,
269 "cu_logmon started ({} components)", self.component_count
270 );
271
272 #[cfg(all(feature = "std", debug_assertions))]
274 register_live_log_listener(|entry, format_str, param_names| {
275 const PARAM_COLOR: &str = "\x1b[36m"; const RESET: &str = "\x1b[0m";
277
278 let params: Vec<String> = entry.params.iter().map(|v| v.to_string()).collect();
279 let colored_params: Vec<String> = params
280 .iter()
281 .map(|v| format!("{PARAM_COLOR}{v}{RESET}"))
282 .collect();
283 let colored_named: HashMap<String, String> = param_names
284 .iter()
285 .zip(params.iter())
286 .map(|(k, v)| (k.to_string(), format!("{PARAM_COLOR}{v}{RESET}")))
287 .collect();
288
289 if let Ok(msg) =
290 format_message_only(format_str, colored_params.as_slice(), &colored_named)
291 {
292 let level_color = match entry.level {
293 CuLogLevel::Debug => "\x1b[32m", CuLogLevel::Info => "\x1b[90m", CuLogLevel::Warning => "\x1b[93m", CuLogLevel::Error => "\x1b[91m", CuLogLevel::Critical => "\x1b[91m",
298 };
299 let ts_color = "\x1b[34m";
300 let ts = format_timestamp(entry.time);
301 println!(
302 "{ts_color}{ts}{reset} {level_color}[{:?}]{reset} {msg}",
303 entry.level,
304 ts = ts,
305 ts_color = ts_color,
306 level_color = level_color,
307 reset = RESET,
308 msg = msg
309 );
310 }
311 });
312 Ok(())
313 }
314
315 fn process_copperlist(&self, ctx: &CuContext, view: CopperListView<'_>) -> CuResult<()> {
316 let call_start = ctx.recent();
317
318 let snapshot = {
319 let mut window = lock_window(&self.window);
320 window.last_report_at.get_or_insert(call_start);
321
322 window.total_copperlists = window.total_copperlists.saturating_add(1);
323 window.window_copperlists = window.window_copperlists.saturating_add(1);
324
325 if let Some(latency) = end_to_end_latency(view.msgs()) {
326 window.end_to_end.record(latency);
327 }
328
329 for entry in view.entries() {
330 let component_index = entry.component_id.index();
331 if let Some(component_stat) = window.per_component.get_mut(component_index)
332 && let Some(duration) = component_duration(entry.msg)
333 {
334 component_stat.record(duration);
335 } else {
336 debug_assert!(
337 component_index < window.per_component.len(),
338 "cu_logmon: component index {} out of bounds {}",
339 component_index,
340 window.per_component.len()
341 );
342 }
343 }
344
345 let snapshot = self.compute_snapshot(&window, call_start);
346 if snapshot.is_some() {
347 window.reset_window(call_start);
348 }
349 snapshot
350 };
351
352 if let Some(snapshot) = snapshot {
353 let log_start = ctx.recent();
354 let use_color = cfg!(feature = "color_log");
355 let base = format!(
356 "[CL {}] rate {}.{} Hz | top4 {} | e2e p50 {}us p90 {}us p99 {}us max {}us | overhead {}us",
357 snapshot.copperlist_index,
358 snapshot.rate_whole,
359 snapshot.rate_tenths,
360 snapshot.top4,
361 snapshot.e2e_p50_us,
362 snapshot.e2e_p90_us,
363 snapshot.e2e_p99_us,
364 snapshot.e2e_max_us,
365 snapshot.overhead_us,
366 );
367 if use_color {
368 const CL_COLOR: &str = "\x1b[94m"; const LABEL_COLOR: &str = "\x1b[92m"; const SUBLABEL_COLOR: &str = "\x1b[93m"; const COMPONENT_NAME_COLOR: &str = "\x1b[38;5;208m"; const RESET: &str = "\x1b[0m";
374 let colored = format!(
375 "[{cl_color}CL {cl}{reset}] {label}rate{reset} {rate_whole}.{rate_tenths} Hz | {label}top4{reset} {component_color}{top4}{reset} | {label}e2e{reset} {sublabel}p50{reset} {p50}us {sublabel}p90{reset} {p90}us {sublabel}p99{reset} {p99}us {sublabel}max{reset} {max}us | {label}overhead{reset} {overhead}us",
376 cl_color = CL_COLOR,
377 label = LABEL_COLOR,
378 sublabel = SUBLABEL_COLOR,
379 component_color = COMPONENT_NAME_COLOR,
380 reset = RESET,
381 cl = snapshot.copperlist_index,
382 rate_whole = snapshot.rate_whole,
383 rate_tenths = snapshot.rate_tenths,
384 p50 = snapshot.e2e_p50_us,
385 p90 = snapshot.e2e_p90_us,
386 p99 = snapshot.e2e_p99_us,
387 max = snapshot.e2e_max_us,
388 top4 = snapshot.top4,
389 overhead = snapshot.overhead_us,
390 );
391 info!(ctx, "{}", &colored);
392 } else {
393 info!(ctx, "{}", &base);
394 }
395 let log_end = ctx.recent();
396 lock_window(&self.window).last_log_duration = log_end - log_start;
397 }
398
399 Ok(())
400 }
401
402 fn process_error(
403 &self,
404 component_id: ComponentId,
405 step: CuComponentState,
406 error: &CuError,
407 ) -> Decision {
408 let component_name = self.component_name(component_id);
409 error!(
410 "Component {} @ {}: Error: {}.",
411 component_name,
412 component_state_label(&step),
413 error,
414 );
415 Decision::Ignore
416 }
417
418 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
419 #[cfg(all(feature = "std", debug_assertions))]
420 unregister_live_log_listener();
421 Ok(())
422 }
423}