Skip to main content

azul_core/
debug.rs

1//! Unified Debug Logging System for Azul
2//!
3//! This module provides a thread-safe debug logging infrastructure that can be
4//! passed through function arguments to collect debug messages during execution.
5//!
6//! ## Design Philosophy
7//!
8//! 1. **No global state for logging** - All logging goes through explicit parameters
9//! 2. **Zero-cost when disabled** - `Option<&mut DebugLogger>` is `None` in production
10//! 3. **Structured messages** - Each message has level, category, and location
11//! 4. **Thread-safe collection** - Can collect messages from multiple threads
12//!
13//! ## Usage Pattern
14//!
15//! Functions that need debug logging accept `debug_log: &mut Option<DebugLogger>`:
16//!
17//! ```rust,ignore
18//! fn do_layout(
19//!     dom: &Dom,
20//!     // ... other params ...
21//!     debug_log: &mut Option<DebugLogger>,
22//! ) {
23//!     log_debug!(debug_log, Layout, "Starting layout for {} nodes", dom.len());
24//!     // ... do work ...
25//! }
26//! ```
27//!
28//! ## Integration with HTTP Debug Server
29//!
30//! When `AZUL_DEBUG` is set, the debug server creates a `DebugLogger` for each
31//! incoming request, which collects all messages until the frame is rendered,
32//! then returns them in the HTTP response.
33
34use alloc::string::String;
35use alloc::vec::Vec;
36
37#[cfg(feature = "std")]
38use std::sync::{Arc, Mutex};
39
40/// Debug message severity level
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[repr(C)]
43pub enum DebugLevel {
44    /// Very detailed tracing information
45    Trace,
46    /// Debugging information
47    Debug,
48    /// General information
49    Info,
50    /// Warnings (potential issues)
51    Warn,
52    /// Errors
53    Error,
54}
55
56impl Default for DebugLevel {
57    fn default() -> Self {
58        DebugLevel::Debug
59    }
60}
61
62/// Categories for debug messages to enable filtering
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
64#[repr(C)]
65pub enum DebugCategory {
66    /// General/uncategorized
67    General,
68    /// Window creation and management
69    Window,
70    /// Event loop processing
71    EventLoop,
72    /// Input events (mouse, keyboard, touch)
73    Input,
74    /// Layout calculation
75    Layout,
76    /// Text shaping and rendering
77    Text,
78    /// Display list generation
79    DisplayList,
80    /// WebRender scene building
81    SceneBuilding,
82    /// GPU rendering
83    Rendering,
84    /// Resource loading (fonts, images)
85    Resources,
86    /// Callbacks and user code
87    Callbacks,
88    /// Timer and animation
89    Timer,
90    /// HTTP debug server
91    DebugServer,
92    /// Platform-specific (macOS, Windows, Linux)
93    Platform,
94    /// Icon resolution
95    Icon,
96}
97
98impl Default for DebugCategory {
99    fn default() -> Self {
100        DebugCategory::General
101    }
102}
103
104/// A structured log message for the debug logger
105#[derive(Debug, Clone, PartialEq)]
106#[repr(C)]
107pub struct LogMessage {
108    /// Severity level
109    pub level: DebugLevel,
110    /// Category for filtering
111    pub category: DebugCategory,
112    /// The message content
113    pub message: String,
114    /// Source file and line (from #[track_caller])
115    pub location: String,
116    /// Elapsed time in microseconds since logger creation
117    pub elapsed_us: u64,
118    /// Optional window ID this message relates to
119    pub window_id: Option<String>,
120}
121
122/// Debug logger that collects messages during execution.
123///
124/// Passed as `&mut Option<DebugLogger>` to functions:
125/// - `None` = logging disabled (production mode)
126/// - `Some(logger)` = logging enabled (debug mode)
127#[cfg(feature = "std")]
128pub struct DebugLogger {
129    messages: Vec<LogMessage>,
130    start_time: std::time::Instant,
131    /// Minimum level to record (messages below this are ignored)
132    min_level: DebugLevel,
133    /// If set, only messages from these categories are recorded
134    category_filter: Option<Vec<DebugCategory>>,
135    /// Current window context (set when processing window-specific events)
136    current_window_id: Option<String>,
137}
138
139#[cfg(feature = "std")]
140impl DebugLogger {
141    /// Create a new debug logger that records all messages
142    pub fn new() -> Self {
143        Self {
144            messages: Vec::new(),
145            start_time: std::time::Instant::now(),
146            min_level: DebugLevel::Trace,
147            category_filter: None,
148            current_window_id: None,
149        }
150    }
151
152    /// Create a logger with minimum level filter
153    pub fn with_min_level(min_level: DebugLevel) -> Self {
154        Self {
155            messages: Vec::new(),
156            start_time: std::time::Instant::now(),
157            min_level,
158            category_filter: None,
159            current_window_id: None,
160        }
161    }
162
163    /// Create a logger that only records specific categories
164    pub fn with_categories(categories: Vec<DebugCategory>) -> Self {
165        Self {
166            messages: Vec::new(),
167            start_time: std::time::Instant::now(),
168            min_level: DebugLevel::Trace,
169            category_filter: Some(categories),
170            current_window_id: None,
171        }
172    }
173
174    /// Set the current window context for subsequent messages
175    pub fn set_window_context(&mut self, window_id: Option<String>) {
176        self.current_window_id = window_id;
177    }
178
179    /// Log a message with full control over all fields
180    #[track_caller]
181    pub fn log(&mut self, level: DebugLevel, category: DebugCategory, message: impl Into<String>) {
182        // Check level filter
183        if level < self.min_level {
184            return;
185        }
186
187        // Check category filter
188        if let Some(ref allowed) = self.category_filter {
189            if !allowed.contains(&category) {
190                return;
191            }
192        }
193
194        let location = core::panic::Location::caller();
195        self.messages.push(LogMessage {
196            level,
197            category,
198            message: message.into(),
199            location: format!("{}:{}", location.file(), location.line()),
200            elapsed_us: self.start_time.elapsed().as_micros() as u64,
201            window_id: self.current_window_id.clone(),
202        });
203    }
204
205    /// Log a trace message
206    #[track_caller]
207    pub fn trace(&mut self, category: DebugCategory, message: impl Into<String>) {
208        self.log(DebugLevel::Trace, category, message);
209    }
210
211    /// Log a debug message
212    #[track_caller]
213    pub fn debug(&mut self, category: DebugCategory, message: impl Into<String>) {
214        self.log(DebugLevel::Debug, category, message);
215    }
216
217    /// Log an info message
218    #[track_caller]
219    pub fn info(&mut self, category: DebugCategory, message: impl Into<String>) {
220        self.log(DebugLevel::Info, category, message);
221    }
222
223    /// Log a warning
224    #[track_caller]
225    pub fn warn(&mut self, category: DebugCategory, message: impl Into<String>) {
226        self.log(DebugLevel::Warn, category, message);
227    }
228
229    /// Log an error
230    #[track_caller]
231    pub fn error(&mut self, category: DebugCategory, message: impl Into<String>) {
232        self.log(DebugLevel::Error, category, message);
233    }
234
235    /// Take all collected messages (empties the logger)
236    pub fn take_messages(&mut self) -> Vec<LogMessage> {
237        core::mem::take(&mut self.messages)
238    }
239
240    /// Get a reference to collected messages
241    pub fn messages(&self) -> &[LogMessage] {
242        &self.messages
243    }
244
245    /// Get count of collected messages
246    pub fn len(&self) -> usize {
247        self.messages.len()
248    }
249
250    /// Check if no messages have been collected
251    pub fn is_empty(&self) -> bool {
252        self.messages.is_empty()
253    }
254
255    /// Get elapsed time since logger was created
256    pub fn elapsed(&self) -> std::time::Duration {
257        self.start_time.elapsed()
258    }
259}
260
261#[cfg(feature = "std")]
262impl Default for DebugLogger {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268/// Convenience macro for logging with automatic category and format
269///
270/// Usage:
271/// ```rust,ignore
272/// log_debug!(logger, Layout, "Processing {} nodes", count);
273/// log_info!(logger, Window, "Window created with id {}", id);
274/// ```
275#[macro_export]
276macro_rules! log_trace {
277    ($logger:expr, $category:ident, $($arg:tt)*) => {
278        if let Some(ref mut logger) = $logger {
279            logger.trace($crate::debug::DebugCategory::$category, format!($($arg)*));
280        }
281    };
282}
283
284#[macro_export]
285macro_rules! log_debug {
286    ($logger:expr, $category:ident, $($arg:tt)*) => {
287        if let Some(ref mut logger) = $logger {
288            logger.debug($crate::debug::DebugCategory::$category, format!($($arg)*));
289        }
290    };
291}
292
293#[macro_export]
294macro_rules! log_info {
295    ($logger:expr, $category:ident, $($arg:tt)*) => {
296        if let Some(ref mut logger) = $logger {
297            logger.info($crate::debug::DebugCategory::$category, format!($($arg)*));
298        }
299    };
300}
301
302#[macro_export]
303macro_rules! log_warn {
304    ($logger:expr, $category:ident, $($arg:tt)*) => {
305        if let Some(ref mut logger) = $logger {
306            logger.warn($crate::debug::DebugCategory::$category, format!($($arg)*));
307        }
308    };
309}
310
311#[macro_export]
312macro_rules! log_error {
313    ($logger:expr, $category:ident, $($arg:tt)*) => {
314        if let Some(ref mut logger) = $logger {
315            logger.error($crate::debug::DebugCategory::$category, format!($($arg)*));
316        }
317    };
318}
319
320/// Type alias for the debug logger parameter pattern used throughout the codebase
321#[cfg(feature = "std")]
322pub type DebugLog = Option<DebugLogger>;
323
324/// Helper function to conditionally log (when logger is Some)
325#[cfg(feature = "std")]
326#[track_caller]
327pub fn debug_log(
328    logger: &mut Option<DebugLogger>,
329    level: DebugLevel,
330    category: DebugCategory,
331    message: impl Into<String>,
332) {
333    if let Some(ref mut l) = logger {
334        l.log(level, category, message);
335    }
336}
337
338/// Check if debug mode is enabled via environment variable
339#[cfg(feature = "std")]
340pub fn is_debug_enabled() -> bool {
341    std::env::var("AZUL_DEBUG").is_ok()
342}
343
344/// Get the debug server port from environment variable
345#[cfg(feature = "std")]
346pub fn get_debug_port() -> Option<u16> {
347    std::env::var("AZUL_DEBUG")
348        .ok()
349        .and_then(|s| s.parse().ok())
350}