Skip to main content

copp\diag/
diagnostics.rs

1//! Diagnostics utilities for TOPP algorithms.
2
3use std::fs::{File, OpenOptions, create_dir_all};
4use std::io::Write;
5use std::path::Path;
6use std::sync::Mutex;
7use std::sync::atomic::{AtomicU8, Ordering};
8use std::time::{Duration, Instant};
9
10use super::error::CoppError;
11
12/// Output backend used by verbosity logging.
13///
14/// Default is [`Println`](VerbosityOutput::Println) to preserve current behavior.
15#[derive(Default, Eq, PartialEq, Clone, Debug)]
16pub enum VerbosityOutput {
17    /// Emit messages with `println!`.
18    ///
19    /// Typical usage:
20    /// ```rust,no_run
21    /// use copp::diag::{VerbosityOutput, set_verbosity_output};
22    ///
23    /// set_verbosity_output(VerbosityOutput::Println)?;
24    /// # Ok::<(), copp::diag::CoppError>(())
25    /// ```
26    #[default]
27    Println,
28    /// Emit messages through the `log` facade (`info/debug/trace`).
29    ///
30    /// Before selecting this mode, initialize a global logger in your app/test.
31    ///
32    /// Typical usage:
33    /// ```rust,no_run
34    /// use log::LevelFilter;
35    /// use std::sync::Once;
36    /// use copp::prelude::{VerbosityOutput, set_verbosity_output};
37    ///
38    /// static INIT: Once = Once::new();
39    /// INIT.call_once(|| {
40    ///     let _ = env_logger::Builder::new()
41    ///         .filter_level(LevelFilter::Info)
42    ///         .is_test(true)
43    ///         .try_init();
44    /// });
45    /// set_verbosity_output(VerbosityOutput::Log)?;
46    /// # Ok::<(), copp::diag::CoppError>(())
47    /// ```
48    Log,
49    /// Emit messages to a configured log file.
50    ///
51    /// Typical usage:
52    /// ```rust,no_run
53    /// use copp::prelude::{VerbosityOutput, set_verbosity_output};
54    ///
55    /// // Auto-creates parent directories and opens file in append mode.
56    /// set_verbosity_output(VerbosityOutput::File("logs/copp/run.log".into()))?;
57    /// # Ok::<(), copp::prelude::CoppError>(())
58    /// ```
59    File(String),
60}
61
62const VERBOSITY_MODE_PRINTLN: u8 = 0;
63const VERBOSITY_MODE_LOG: u8 = 1;
64const VERBOSITY_MODE_FILE: u8 = 2;
65
66static VERBOSITY_OUTPUT: AtomicU8 = AtomicU8::new(VERBOSITY_MODE_PRINTLN);
67static VERBOSITY_FILE: Mutex<Option<File>> = Mutex::new(None);
68static VERBOSITY_FILE_PATH: Mutex<Option<String>> = Mutex::new(None);
69
70/// Set output backend for all verbosity messages.
71///
72/// This is a global process-wide switch.
73pub fn set_verbosity_output(output: VerbosityOutput) -> Result<(), CoppError> {
74    match output {
75        VerbosityOutput::Println => {
76            VERBOSITY_OUTPUT.store(VERBOSITY_MODE_PRINTLN, Ordering::Relaxed);
77            Ok(())
78        }
79        VerbosityOutput::Log => {
80            VERBOSITY_OUTPUT.store(VERBOSITY_MODE_LOG, Ordering::Relaxed);
81            Ok(())
82        }
83        VerbosityOutput::File(path) => set_verbosity_log_file(path),
84    }
85}
86
87/// Get current output backend for verbosity messages.
88pub fn verbosity_output() -> VerbosityOutput {
89    match VERBOSITY_OUTPUT.load(Ordering::Relaxed) {
90        VERBOSITY_MODE_LOG => VerbosityOutput::Log,
91        VERBOSITY_MODE_FILE => {
92            let path = VERBOSITY_FILE_PATH
93                .lock()
94                .expect("verbosity_output: mutex poisoned")
95                .clone()
96                .unwrap_or_else(|| "<unknown>".to_string());
97            VerbosityOutput::File(path)
98        }
99        _ => VerbosityOutput::Println,
100    }
101}
102
103/// Configure a log file as verbosity output backend.
104///
105/// The parent directory is created automatically if it does not exist.
106/// File is opened in append mode.
107pub fn set_verbosity_log_file(path: impl AsRef<Path>) -> Result<(), CoppError> {
108    let path = path.as_ref();
109    if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
110        create_dir_all(parent)?;
111    }
112    let file = OpenOptions::new().create(true).append(true).open(path)?;
113    let path_string = path.to_string_lossy().to_string();
114    let mut file_slot = VERBOSITY_FILE
115        .lock()
116        .expect("set_verbosity_log_file: mutex poisoned");
117    let mut path_slot = VERBOSITY_FILE_PATH
118        .lock()
119        .expect("set_verbosity_log_file(path): mutex poisoned");
120    *file_slot = Some(file);
121    *path_slot = Some(path_string);
122    VERBOSITY_OUTPUT.store(VERBOSITY_MODE_FILE, Ordering::Relaxed);
123    Ok(())
124}
125
126/// The verbosity level for logging.  
127/// The default value is [`Silent`](`Verbosity::Silent`).  
128/// [`Trace`](`Verbosity::Trace`) > [`Debug`](`Verbosity::Debug`) > [`Summary`](`Verbosity::Summary`) > [`Silent`](`Verbosity::Silent`).
129#[repr(u8)]
130#[derive(Default, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)]
131pub enum Verbosity {
132    /// No log will be emitted.
133    #[default]
134    Silent = 0,
135    /// Only summary information will be emitted, including:  
136    /// + The beginning and end of each algorithm.  
137    /// + The total computation time of each algorithm.  
138    /// + The success or failure of each algorithm.
139    Summary = 1,
140    /// Detailed debug information will be emitted, including:  
141    /// + The failure or degeneration of each algorithm at each grid point.
142    Debug = 2,
143    /// Very detailed trace information will be emitted, including:  
144    /// + The values of reachable sets, optimal profiles, and other intermediate variables at each step of each algorithm.
145    Trace = 3,
146}
147
148/// Emit one verbosity message using the configured backend.
149pub fn emit_verbosity_line(level: Verbosity, message: impl AsRef<str>) {
150    let message = message.as_ref();
151    match verbosity_output() {
152        VerbosityOutput::Println => println!("{message}"),
153        VerbosityOutput::Log => match level {
154            Verbosity::Silent => {}
155            Verbosity::Summary => log::info!("{message}"),
156            Verbosity::Debug => log::debug!("{message}"),
157            Verbosity::Trace => log::trace!("{message}"),
158        },
159        VerbosityOutput::File(_) => {
160            if matches!(level, Verbosity::Silent) {
161                return;
162            }
163            let mut file_slot = VERBOSITY_FILE
164                .lock()
165                .expect("emit_verbosity_line: mutex poisoned");
166            if let Some(file) = file_slot.as_mut() {
167                let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
168                let level_tag = match level {
169                    Verbosity::Silent => "SILENT",
170                    Verbosity::Summary => "INFO",
171                    Verbosity::Debug => "DEBUG",
172                    Verbosity::Trace => "TRACE",
173                };
174                let _ = writeln!(file, "[{now}] [{level_tag}] {message}");
175                let _ = file.flush();
176            } else {
177                println!("{message}");
178            }
179        }
180    }
181}
182
183/// Emit a formatted verbosity message through the global diagnostics backend.
184///
185/// This macro is a convenience wrapper over [`emit_verbosity_line`](crate::diag::emit_verbosity_line).
186/// It accepts a [`Verbosity`](crate::diag::Verbosity) level plus `format!`-style arguments.
187///
188/// # Example
189/// ```rust,no_run
190/// use copp::diag::Verbosity;
191///
192/// copp::verbosity_log!(Verbosity::Summary, "solver finished in {:.3} ms", 12.34);
193/// ```
194#[macro_export]
195macro_rules! verbosity_log {
196    ($level:expr, $($arg:tt)*) => {
197        $crate::diag::emit_verbosity_line($level, format!($($arg)*))
198    };
199}
200
201pub(crate) trait Verboser {
202    /// The verbosity level for logging.
203    const LEVEL: Verbosity;
204    /// Check if the current verbosity level is greater than or equal to the given level.
205    #[inline(always)]
206    fn is_enabled(&self, level: Verbosity) -> bool {
207        Self::LEVEL >= level
208    }
209    fn record_start_time(&mut self);
210    fn elapsed(&self) -> Duration;
211}
212
213pub(crate) struct SilentVerboser;
214
215impl Verboser for SilentVerboser {
216    const LEVEL: Verbosity = Verbosity::Silent;
217    #[inline(always)]
218    fn record_start_time(&mut self) {}
219    #[inline(always)]
220    fn elapsed(&self) -> Duration {
221        Duration::ZERO
222    }
223}
224pub(crate) struct SummaryVerboser {
225    start_time: Instant,
226}
227impl SummaryVerboser {
228    pub fn new() -> Self {
229        Self {
230            start_time: Instant::now(),
231        }
232    }
233}
234impl Verboser for SummaryVerboser {
235    const LEVEL: Verbosity = Verbosity::Summary;
236    #[inline(always)]
237    fn record_start_time(&mut self) {
238        self.start_time = Instant::now();
239    }
240    #[inline(always)]
241    fn elapsed(&self) -> Duration {
242        self.start_time.elapsed()
243    }
244}
245pub(crate) struct DebugVerboser {
246    start_time: Instant,
247}
248impl DebugVerboser {
249    pub fn new() -> Self {
250        Self {
251            start_time: Instant::now(),
252        }
253    }
254}
255impl Verboser for DebugVerboser {
256    const LEVEL: Verbosity = Verbosity::Debug;
257    #[inline(always)]
258    fn record_start_time(&mut self) {
259        self.start_time = Instant::now();
260    }
261    #[inline(always)]
262    fn elapsed(&self) -> Duration {
263        self.start_time.elapsed()
264    }
265}
266pub(crate) struct TraceVerboser {
267    start_time: Instant,
268}
269impl TraceVerboser {
270    pub fn new() -> Self {
271        Self {
272            start_time: Instant::now(),
273        }
274    }
275}
276impl Verboser for TraceVerboser {
277    const LEVEL: Verbosity = Verbosity::Trace;
278    #[inline(always)]
279    fn record_start_time(&mut self) {
280        self.start_time = Instant::now();
281    }
282    #[inline(always)]
283    fn elapsed(&self) -> Duration {
284        self.start_time.elapsed()
285    }
286}
287
288/// Format a `Duration` into a human-readable string with appropriate units (ns, us, ms, s, min, h).
289pub(crate) fn format_duration_human(d: Duration) -> String {
290    let secs = d.as_secs_f64();
291
292    fn fmt(v: f64, unit: &str) -> String {
293        if v >= 100.0 {
294            format!("{v:.0} {unit}")
295        } else if v >= 10.0 {
296            format!("{v:.1} {unit}")
297        } else {
298            format!("{v:.3} {unit}")
299        }
300    }
301
302    if secs < 1e-6 {
303        fmt(secs * 1e9, "ns")
304    } else if secs < 1e-3 {
305        fmt(secs * 1e6, "us")
306    } else if secs < 1.0 {
307        fmt(secs * 1e3, "ms")
308    } else if secs < 60.0 {
309        fmt(secs, "s")
310    } else if secs < 3600.0 {
311        fmt(secs / 60.0, "min")
312    } else {
313        fmt(secs / 3600.0, "h")
314    }
315}