Skip to main content

scirs2_core/logging/
bridge.rs

1//! Integration bridge between SciRS2 logging and the `log` crate facade.
2//!
3//! This module provides:
4//! - `LogCrateBridge` -- forwards SciRS2 `Logger` entries to `log` macros
5//! - `scirs2_time!` macro -- times an expression and logs the elapsed duration
6//! - `MemoryTracker` -- lightweight memory usage tracking utility
7//!
8//! # Feature Gate
9//!
10//! The bridge to the `log` crate requires the `logging` feature to be enabled
11//! on `scirs2-core`. If the feature is not enabled, the `log` forwarding
12//! functionality will not produce output unless a `log` subscriber is
13//! installed externally.
14//!
15//! # Usage
16//!
17//! ```rust,no_run
18//! use scirs2_core::logging::bridge::{LogCrateBridge, MemoryTracker};
19//!
20//! // Install the bridge so SciRS2 logs flow to the `log` crate
21//! LogCrateBridge::install();
22//!
23//! // Use the timing utility
24//! let tracker = MemoryTracker::new();
25//! tracker.record_allocation(1024);
26//! let report = tracker.report();
27//! ```
28
29use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
30use std::sync::Arc;
31use std::time::Instant;
32
33use super::{LogEntry, LogHandler, LogLevel};
34
35// ---------------------------------------------------------------------------
36// LogCrateBridge -- forwards SciRS2 Logger entries to the `log` crate
37// ---------------------------------------------------------------------------
38
39/// A `LogHandler` that forwards SciRS2 log entries to the standard `log` crate.
40///
41/// Install it once via [`LogCrateBridge::install()`] and all SciRS2 internal
42/// log messages will be emitted through whatever `log` subscriber the
43/// application has configured (e.g., `env_logger`, `tracing-log`, etc.).
44#[derive(Debug)]
45pub struct LogCrateBridge;
46
47impl LogCrateBridge {
48    /// Register this bridge as a SciRS2 log handler.
49    ///
50    /// Safe to call multiple times; each call adds another handler, so prefer
51    /// calling this exactly once at application startup.
52    pub fn install() {
53        let handler = Arc::new(LogCrateBridge);
54        super::set_handler(handler);
55    }
56
57    /// Map SciRS2 `LogLevel` to `log::Level`.
58    fn map_level(level: LogLevel) -> log::Level {
59        match level {
60            LogLevel::Trace => log::Level::Trace,
61            LogLevel::Debug => log::Level::Debug,
62            LogLevel::Info => log::Level::Info,
63            LogLevel::Warn => log::Level::Warn,
64            LogLevel::Error | LogLevel::Critical => log::Level::Error,
65        }
66    }
67}
68
69impl LogHandler for LogCrateBridge {
70    fn handle(&self, entry: &LogEntry) {
71        let level = Self::map_level(entry.level);
72        // Use the `log` crate's logging macro to forward the message.
73        // We build a record manually so we can set the target (module).
74        log::log!(target: &entry.module, level, "{}", entry.message);
75    }
76}
77
78// ---------------------------------------------------------------------------
79// scirs2_time! macro -- performance timing
80// ---------------------------------------------------------------------------
81
82/// Times the execution of an expression and logs the elapsed duration.
83///
84/// # Examples
85///
86/// ```rust,no_run
87/// use scirs2_core::scirs2_time;
88///
89/// let result = scirs2_time!("matrix_multiply", {
90///     // expensive operation
91///     42
92/// });
93/// assert_eq!(result, 42);
94/// ```
95///
96/// With an explicit logger:
97///
98/// ```rust,no_run
99/// use scirs2_core::scirs2_time;
100/// use scirs2_core::logging::Logger;
101///
102/// let logger = Logger::new("my_module");
103/// let result = scirs2_time!(logger, "decomposition", {
104///     100
105/// });
106/// assert_eq!(result, 100);
107/// ```
108#[macro_export]
109macro_rules! scirs2_time {
110    ($label:expr, $body:expr) => {{
111        let _start = std::time::Instant::now();
112        let _result = $body;
113        let _elapsed = _start.elapsed();
114        // Use the log crate if available
115        log::info!(
116            target: "scirs2::timing",
117            "[TIMING] {}: {:.6}s ({:.3}ms)",
118            $label,
119            _elapsed.as_secs_f64(),
120            _elapsed.as_secs_f64() * 1000.0
121        );
122        _result
123    }};
124    ($logger:expr, $label:expr, $body:expr) => {{
125        let _start = std::time::Instant::now();
126        let _result = $body;
127        let _elapsed = _start.elapsed();
128        $logger.info(&format!(
129            "[TIMING] {}: {:.6}s ({:.3}ms)",
130            $label,
131            _elapsed.as_secs_f64(),
132            _elapsed.as_secs_f64() * 1000.0
133        ));
134        _result
135    }};
136}
137
138// ---------------------------------------------------------------------------
139// TimingGuard -- RAII-based timing
140// ---------------------------------------------------------------------------
141
142/// RAII guard that measures wall-clock time from construction to drop.
143///
144/// Useful for timing scopes without wrapping in a macro.
145///
146/// ```rust,no_run
147/// use scirs2_core::logging::bridge::TimingGuard;
148///
149/// {
150///     let _guard = TimingGuard::new("expensive_scope");
151///     // ... work happens here ...
152/// } // logs elapsed time on drop
153/// ```
154pub struct TimingGuard {
155    label: String,
156    start: Instant,
157}
158
159impl TimingGuard {
160    /// Create a new timing guard with the given label.
161    pub fn new(label: &str) -> Self {
162        Self {
163            label: label.to_string(),
164            start: Instant::now(),
165        }
166    }
167
168    /// Elapsed time since construction.
169    pub fn elapsed(&self) -> std::time::Duration {
170        self.start.elapsed()
171    }
172}
173
174impl Drop for TimingGuard {
175    fn drop(&mut self) {
176        let elapsed = self.start.elapsed();
177        log::info!(
178            target: "scirs2::timing",
179            "[TIMING] {}: {:.6}s ({:.3}ms)",
180            self.label,
181            elapsed.as_secs_f64(),
182            elapsed.as_secs_f64() * 1000.0
183        );
184    }
185}
186
187// ---------------------------------------------------------------------------
188// MemoryTracker -- lightweight memory usage tracking
189// ---------------------------------------------------------------------------
190
191/// Thread-safe memory usage tracker.
192///
193/// Records allocations and deallocations so callers can inspect high-water-mark
194/// and current usage without relying on a system allocator hook.
195#[derive(Debug, Clone)]
196pub struct MemoryTracker {
197    inner: Arc<MemoryTrackerInner>,
198}
199
200#[derive(Debug)]
201struct MemoryTrackerInner {
202    /// Current live bytes (may temporarily go negative in racy scenarios, stored as i64 via AtomicU64)
203    current_bytes: AtomicU64,
204    /// Peak (high-water mark) bytes
205    peak_bytes: AtomicU64,
206    /// Total bytes ever allocated
207    total_allocated: AtomicU64,
208    /// Total bytes ever freed
209    total_freed: AtomicU64,
210    /// Number of allocation events
211    allocation_count: AtomicUsize,
212    /// Number of deallocation events
213    deallocation_count: AtomicUsize,
214}
215
216/// A snapshot report of memory usage.
217#[derive(Debug, Clone)]
218pub struct MemoryReport {
219    /// Current live bytes
220    pub current_bytes: u64,
221    /// Peak bytes seen
222    pub peak_bytes: u64,
223    /// Total allocated over lifetime
224    pub total_allocated: u64,
225    /// Total freed over lifetime
226    pub total_freed: u64,
227    /// Number of allocation events
228    pub allocation_count: usize,
229    /// Number of deallocation events
230    pub deallocation_count: usize,
231}
232
233impl std::fmt::Display for MemoryReport {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        writeln!(f, "MemoryReport {{")?;
236        writeln!(
237            f,
238            "  current:   {} bytes ({:.2} MiB)",
239            self.current_bytes,
240            self.current_bytes as f64 / (1024.0 * 1024.0)
241        )?;
242        writeln!(
243            f,
244            "  peak:      {} bytes ({:.2} MiB)",
245            self.peak_bytes,
246            self.peak_bytes as f64 / (1024.0 * 1024.0)
247        )?;
248        writeln!(
249            f,
250            "  allocated: {} bytes ({}x)",
251            self.total_allocated, self.allocation_count
252        )?;
253        writeln!(
254            f,
255            "  freed:     {} bytes ({}x)",
256            self.total_freed, self.deallocation_count
257        )?;
258        write!(f, "}}")
259    }
260}
261
262impl MemoryTracker {
263    /// Create a new memory tracker with all counters at zero.
264    pub fn new() -> Self {
265        Self {
266            inner: Arc::new(MemoryTrackerInner {
267                current_bytes: AtomicU64::new(0),
268                peak_bytes: AtomicU64::new(0),
269                total_allocated: AtomicU64::new(0),
270                total_freed: AtomicU64::new(0),
271                allocation_count: AtomicUsize::new(0),
272                deallocation_count: AtomicUsize::new(0),
273            }),
274        }
275    }
276
277    /// Record an allocation of `bytes` bytes.
278    pub fn record_allocation(&self, bytes: u64) {
279        self.inner
280            .total_allocated
281            .fetch_add(bytes, Ordering::Relaxed);
282        self.inner.allocation_count.fetch_add(1, Ordering::Relaxed);
283        let new_current = self.inner.current_bytes.fetch_add(bytes, Ordering::Relaxed) + bytes;
284        // Update peak via CAS loop
285        let mut peak = self.inner.peak_bytes.load(Ordering::Relaxed);
286        while new_current > peak {
287            match self.inner.peak_bytes.compare_exchange_weak(
288                peak,
289                new_current,
290                Ordering::Relaxed,
291                Ordering::Relaxed,
292            ) {
293                Ok(_) => break,
294                Err(actual) => peak = actual,
295            }
296        }
297    }
298
299    /// Record a deallocation of `bytes` bytes.
300    pub fn record_deallocation(&self, bytes: u64) {
301        self.inner.total_freed.fetch_add(bytes, Ordering::Relaxed);
302        self.inner
303            .deallocation_count
304            .fetch_add(1, Ordering::Relaxed);
305        // Saturating subtract to avoid underflow
306        let _ = self.inner.current_bytes.fetch_update(
307            Ordering::Relaxed,
308            Ordering::Relaxed,
309            |current| Some(current.saturating_sub(bytes)),
310        );
311    }
312
313    /// Get the current live bytes.
314    pub fn current_bytes(&self) -> u64 {
315        self.inner.current_bytes.load(Ordering::Relaxed)
316    }
317
318    /// Get the peak (high-water mark) bytes.
319    pub fn peak_bytes(&self) -> u64 {
320        self.inner.peak_bytes.load(Ordering::Relaxed)
321    }
322
323    /// Take a snapshot report.
324    pub fn report(&self) -> MemoryReport {
325        MemoryReport {
326            current_bytes: self.inner.current_bytes.load(Ordering::Relaxed),
327            peak_bytes: self.inner.peak_bytes.load(Ordering::Relaxed),
328            total_allocated: self.inner.total_allocated.load(Ordering::Relaxed),
329            total_freed: self.inner.total_freed.load(Ordering::Relaxed),
330            allocation_count: self.inner.allocation_count.load(Ordering::Relaxed),
331            deallocation_count: self.inner.deallocation_count.load(Ordering::Relaxed),
332        }
333    }
334
335    /// Reset all counters to zero.
336    pub fn reset(&self) {
337        self.inner.current_bytes.store(0, Ordering::Relaxed);
338        self.inner.peak_bytes.store(0, Ordering::Relaxed);
339        self.inner.total_allocated.store(0, Ordering::Relaxed);
340        self.inner.total_freed.store(0, Ordering::Relaxed);
341        self.inner.allocation_count.store(0, Ordering::Relaxed);
342        self.inner.deallocation_count.store(0, Ordering::Relaxed);
343    }
344}
345
346impl Default for MemoryTracker {
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352// ---------------------------------------------------------------------------
353// Tests
354// ---------------------------------------------------------------------------
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_log_crate_bridge_level_mapping() {
362        assert_eq!(
363            LogCrateBridge::map_level(LogLevel::Trace),
364            log::Level::Trace
365        );
366        assert_eq!(
367            LogCrateBridge::map_level(LogLevel::Debug),
368            log::Level::Debug
369        );
370        assert_eq!(LogCrateBridge::map_level(LogLevel::Info), log::Level::Info);
371        assert_eq!(LogCrateBridge::map_level(LogLevel::Warn), log::Level::Warn);
372        assert_eq!(
373            LogCrateBridge::map_level(LogLevel::Error),
374            log::Level::Error
375        );
376        assert_eq!(
377            LogCrateBridge::map_level(LogLevel::Critical),
378            log::Level::Error
379        );
380    }
381
382    #[test]
383    fn test_memory_tracker_allocation() {
384        let tracker = MemoryTracker::new();
385        assert_eq!(tracker.current_bytes(), 0);
386
387        tracker.record_allocation(1024);
388        assert_eq!(tracker.current_bytes(), 1024);
389        assert_eq!(tracker.peak_bytes(), 1024);
390
391        tracker.record_allocation(2048);
392        assert_eq!(tracker.current_bytes(), 3072);
393        assert_eq!(tracker.peak_bytes(), 3072);
394    }
395
396    #[test]
397    fn test_memory_tracker_deallocation() {
398        let tracker = MemoryTracker::new();
399        tracker.record_allocation(1024);
400        tracker.record_allocation(2048);
401
402        tracker.record_deallocation(1024);
403        assert_eq!(tracker.current_bytes(), 2048);
404        // Peak should not decrease
405        assert_eq!(tracker.peak_bytes(), 3072);
406    }
407
408    #[test]
409    fn test_memory_tracker_deallocation_underflow_protection() {
410        let tracker = MemoryTracker::new();
411        tracker.record_allocation(100);
412        tracker.record_deallocation(200); // More than allocated
413        assert_eq!(tracker.current_bytes(), 0); // Should saturate at 0
414    }
415
416    #[test]
417    fn test_memory_tracker_report() {
418        let tracker = MemoryTracker::new();
419        tracker.record_allocation(1000);
420        tracker.record_allocation(2000);
421        tracker.record_deallocation(500);
422
423        let report = tracker.report();
424        assert_eq!(report.current_bytes, 2500);
425        assert_eq!(report.peak_bytes, 3000);
426        assert_eq!(report.total_allocated, 3000);
427        assert_eq!(report.total_freed, 500);
428        assert_eq!(report.allocation_count, 2);
429        assert_eq!(report.deallocation_count, 1);
430    }
431
432    #[test]
433    fn test_memory_tracker_reset() {
434        let tracker = MemoryTracker::new();
435        tracker.record_allocation(5000);
436        tracker.reset();
437
438        let report = tracker.report();
439        assert_eq!(report.current_bytes, 0);
440        assert_eq!(report.peak_bytes, 0);
441        assert_eq!(report.total_allocated, 0);
442        assert_eq!(report.allocation_count, 0);
443    }
444
445    #[test]
446    fn test_memory_tracker_clone_shares_state() {
447        let tracker1 = MemoryTracker::new();
448        let tracker2 = tracker1.clone();
449
450        tracker1.record_allocation(1024);
451        assert_eq!(tracker2.current_bytes(), 1024);
452    }
453
454    #[test]
455    fn test_memory_report_display() {
456        let tracker = MemoryTracker::new();
457        tracker.record_allocation(1_048_576); // 1 MiB
458        let report = tracker.report();
459        let display = format!("{report}");
460        assert!(display.contains("1.00 MiB"));
461        assert!(display.contains("1048576"));
462    }
463
464    #[test]
465    fn test_timing_guard_elapsed() {
466        let guard = TimingGuard::new("test_op");
467        std::thread::sleep(std::time::Duration::from_millis(10));
468        let elapsed = guard.elapsed();
469        assert!(elapsed.as_millis() >= 5, "Should have elapsed at least 5ms");
470        // Drop will log, which is fine in tests
471    }
472
473    #[test]
474    fn test_timing_guard_does_not_panic_on_drop() {
475        {
476            let _guard = TimingGuard::new("scope_test");
477            // Work
478            let _ = 1 + 1;
479        }
480        // If we reach here, drop did not panic
481    }
482
483    #[test]
484    fn test_log_bridge_install_does_not_panic() {
485        // Just ensure installing doesn't panic
486        LogCrateBridge::install();
487    }
488
489    #[test]
490    fn test_memory_tracker_thread_safety() {
491        let tracker = MemoryTracker::new();
492        let handles: Vec<_> = (0..4)
493            .map(|_| {
494                let t = tracker.clone();
495                std::thread::spawn(move || {
496                    for _ in 0..100 {
497                        t.record_allocation(10);
498                    }
499                    for _ in 0..50 {
500                        t.record_deallocation(10);
501                    }
502                })
503            })
504            .collect();
505
506        for h in handles {
507            h.join().expect("Thread should not panic");
508        }
509
510        let report = tracker.report();
511        assert_eq!(report.total_allocated, 4 * 100 * 10);
512        assert_eq!(report.total_freed, 4 * 50 * 10);
513        assert_eq!(report.allocation_count, 400);
514        assert_eq!(report.deallocation_count, 200);
515        assert_eq!(report.current_bytes, 4 * 50 * 10); // 4*100*10 - 4*50*10
516    }
517}