browsing 0.1.3

Lightweight MCP/API for browser automation: navigate, get content (text), screenshot. Parallelism via RwLock.
Documentation
//! Performance monitoring and metrics collection

use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;

/// Performance metrics for browser operations
#[derive(Debug, Clone)]
pub struct Metrics {
    /// CDP command execution metrics
    pub cdp_commands: CdpMetrics,
    /// DOM processing metrics
    pub dom_processing: DomMetrics,
    /// Browser operation metrics
    pub browser_ops: BrowserMetrics,
}

/// Metrics for CDP commands
#[derive(Debug, Clone, Default)]
pub struct CdpMetrics {
    /// Total number of commands executed
    pub total_commands: u64,
    /// Total execution time
    pub total_time: Duration,
    /// Number of successful commands
    pub successful_commands: u64,
    /// Number of failed commands
    pub failed_commands: u64,
    /// Command execution times by method
    pub command_times: HashMap<String, CommandStats>,
}

/// Statistics for a specific command
#[derive(Debug, Clone, Default)]
pub struct CommandStats {
    /// Number of times executed
    pub count: u64,
    /// Total execution time
    pub total_time: Duration,
    /// Minimum execution time
    pub min_time: Duration,
    /// Maximum execution time
    pub max_time: Duration,
}

/// Metrics for DOM processing
#[derive(Debug, Clone, Default)]
pub struct DomMetrics {
    /// Number of DOM trees processed
    pub total_processed: u64,
    /// Total processing time
    pub total_time: Duration,
    /// Number of elements extracted
    pub total_elements: u64,
    /// Number of interactive elements found
    pub interactive_elements: u64,
}

/// Metrics for browser operations
#[derive(Debug, Clone, Default)]
pub struct BrowserMetrics {
    /// Number of navigations
    pub navigations: u64,
    /// Total navigation time
    pub navigation_time: Duration,
    /// Number of screenshots taken
    pub screenshots: u64,
    /// Total screenshot time
    pub screenshot_time: Duration,
}

impl Default for Metrics {
    fn default() -> Self {
        Self {
            cdp_commands: Default::default(),
            dom_processing: Default::default(),
            browser_ops: Default::default(),
        }
    }
}

impl Metrics {
    /// Create a new metrics instance
    pub fn new() -> Self {
        Default::default()
    }

    /// Record a CDP command execution
    pub fn record_cdp_command(&mut self, method: &str, duration: Duration, success: bool) {
        self.cdp_commands.total_commands += 1;
        self.cdp_commands.total_time += duration;
        if success {
            self.cdp_commands.successful_commands += 1;
        } else {
            self.cdp_commands.failed_commands += 1;
        }

        let stats = self
            .cdp_commands
            .command_times
            .entry(method.to_string())
            .or_default();

        stats.count += 1;
        stats.total_time += duration;
        stats.min_time = stats.min_time.min(duration);
        stats.max_time = stats.max_time.max(duration);
    }

    /// Record DOM processing
    pub fn record_dom_processing(&mut self, duration: Duration, elements: u64, interactive: u64) {
        self.dom_processing.total_processed += 1;
        self.dom_processing.total_time += duration;
        self.dom_processing.total_elements += elements;
        self.dom_processing.interactive_elements += interactive;
    }

    /// Record a browser navigation
    pub fn record_navigation(&mut self, duration: Duration) {
        self.browser_ops.navigations += 1;
        self.browser_ops.navigation_time += duration;
    }

    /// Record a screenshot
    pub fn record_screenshot(&mut self, duration: Duration) {
        self.browser_ops.screenshots += 1;
        self.browser_ops.screenshot_time += duration;
    }

    /// Get a summary of all metrics
    pub fn summary(&self) -> MetricsSummary {
        MetricsSummary {
            cdp_avg_time: self._avg_duration(
                self.cdp_commands.total_commands,
                self.cdp_commands.total_time,
            ),
            cdp_success_rate: self._success_rate(
                self.cdp_commands.successful_commands,
                self.cdp_commands.total_commands,
            ),
            dom_avg_time: self._avg_duration(
                self.dom_processing.total_processed,
                self.dom_processing.total_time,
            ),
            dom_avg_elements: self._avg_u64(
                self.dom_processing.total_processed,
                self.dom_processing.total_elements,
            ),
            nav_avg_time: self._avg_duration(
                self.browser_ops.navigations,
                self.browser_ops.navigation_time,
            ),
            screenshot_avg_time: self._avg_duration(
                self.browser_ops.screenshots,
                self.browser_ops.screenshot_time,
            ),
        }
    }

    fn _avg_duration(&self, count: u64, total: Duration) -> Option<Duration> {
        if count > 0 {
            Some(total / count as u32)
        } else {
            None
        }
    }

    fn _avg_u64(&self, count: u64, total: u64) -> Option<f64> {
        if count > 0 {
            Some(total as f64 / count as f64)
        } else {
            None
        }
    }

    fn _success_rate(&self, successful: u64, total: u64) -> Option<f64> {
        if total > 0 {
            Some(successful as f64 / total as f64 * 100.0)
        } else {
            None
        }
    }
}

/// Summary of key performance metrics
#[derive(Debug, Clone)]
pub struct MetricsSummary {
    /// Average CDP command execution time
    pub cdp_avg_time: Option<Duration>,
    /// CDP command success rate (percentage)
    pub cdp_success_rate: Option<f64>,
    /// Average DOM processing time
    pub dom_avg_time: Option<Duration>,
    /// Average number of elements per DOM
    pub dom_avg_elements: Option<f64>,
    /// Average navigation time
    pub nav_avg_time: Option<Duration>,
    /// Average screenshot time
    pub screenshot_avg_time: Option<Duration>,
}

impl std::fmt::Display for MetricsSummary {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Performance Metrics Summary:")?;
        writeln!(f, "  CDP Commands:")?;
        if let Some(avg) = &self.cdp_avg_time {
            writeln!(f, "    Avg Time: {:?}", avg)?;
        }
        if let Some(rate) = &self.cdp_success_rate {
            writeln!(f, "    Success Rate: {:.2}%", rate)?;
        }
        writeln!(f, "  DOM Processing:")?;
        if let Some(avg) = &self.dom_avg_time {
            writeln!(f, "    Avg Time: {:?}", avg)?;
        }
        if let Some(avg) = &self.dom_avg_elements {
            writeln!(f, "    Avg Elements: {:.2}", avg)?;
        }
        writeln!(f, "  Browser Operations:")?;
        if let Some(avg) = &self.nav_avg_time {
            writeln!(f, "    Avg Navigation: {:?}", avg)?;
        }
        if let Some(avg) = &self.screenshot_avg_time {
            writeln!(f, "    Avg Screenshot: {:?}", avg)?;
        }
        Ok(())
    }
}

/// Thread-safe metrics collector
#[derive(Debug, Clone)]
pub struct MetricsCollector {
    metrics: Arc<RwLock<Metrics>>,
}

impl MetricsCollector {
    /// Create a new metrics collector
    pub fn new() -> Self {
        Self {
            metrics: Arc::new(RwLock::new(Metrics::new())),
        }
    }

    /// Record a CDP command execution
    pub async fn record_cdp_command(&self, method: &str, duration: Duration, success: bool) {
        let mut metrics = self.metrics.write().await;
        metrics.record_cdp_command(method, duration, success);
    }

    /// Record DOM processing
    pub async fn record_dom_processing(&self, duration: Duration, elements: u64, interactive: u64) {
        let mut metrics = self.metrics.write().await;
        metrics.record_dom_processing(duration, elements, interactive);
    }

    /// Record a browser navigation
    pub async fn record_navigation(&self, duration: Duration) {
        let mut metrics = self.metrics.write().await;
        metrics.record_navigation(duration);
    }

    /// Record a screenshot
    pub async fn record_screenshot(&self, duration: Duration) {
        let mut metrics = self.metrics.write().await;
        metrics.record_screenshot(duration);
    }

    /// Get a snapshot of current metrics
    pub async fn snapshot(&self) -> Metrics {
        self.metrics.read().await.clone()
    }

    /// Get a summary of current metrics
    pub async fn summary(&self) -> MetricsSummary {
        self.metrics.read().await.summary()
    }

    /// Reset all metrics
    pub async fn reset(&self) {
        *self.metrics.write().await = Metrics::new();
    }
}

impl Default for MetricsCollector {
    fn default() -> Self {
        Self::new()
    }
}

/// Helper to measure execution time
pub struct Timer {
    start: Instant,
}

impl Timer {
    /// Start a new timer
    pub fn start() -> Self {
        Self {
            start: Instant::now(),
        }
    }

    /// Stop the timer and return the elapsed duration
    pub fn stop(self) -> Duration {
        self.start.elapsed()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_metrics_record_cdp_command() {
        let mut metrics = Metrics::new();
        metrics.record_cdp_command("Page.navigate", Duration::from_millis(100), true);
        metrics.record_cdp_command("Page.navigate", Duration::from_millis(150), true);
        metrics.record_cdp_command("Page.navigate", Duration::from_millis(50), false);

        assert_eq!(metrics.cdp_commands.total_commands, 3);
        assert_eq!(metrics.cdp_commands.successful_commands, 2);
        assert_eq!(metrics.cdp_commands.failed_commands, 1);

        let summary = metrics.summary();
        assert!(summary.cdp_avg_time.is_some());
        assert!(summary.cdp_success_rate.is_some());
        assert_eq!(summary.cdp_success_rate.unwrap(), 2.0 / 3.0 * 100.0);
    }

    #[test]
    fn test_metrics_record_dom_processing() {
        let mut metrics = Metrics::new();
        metrics.record_dom_processing(Duration::from_millis(200), 100, 10);
        metrics.record_dom_processing(Duration::from_millis(300), 200, 20);

        assert_eq!(metrics.dom_processing.total_processed, 2);
        assert_eq!(metrics.dom_processing.total_elements, 300);
        assert_eq!(metrics.dom_processing.interactive_elements, 30);

        let summary = metrics.summary();
        assert!(summary.dom_avg_time.is_some());
        assert!(summary.dom_avg_elements.is_some());
    }

    #[test]
    fn test_timer() {
        let timer = Timer::start();
        std::thread::sleep(std::time::Duration::from_millis(10));
        let duration = timer.stop();
        assert!(duration.as_millis() >= 10);
    }

    #[tokio::test]
    async fn test_metrics_collector() {
        let collector = MetricsCollector::new();
        collector
            .record_cdp_command("test", Duration::from_millis(100), true)
            .await;
        collector
            .record_cdp_command("test", Duration::from_millis(200), true)
            .await;

        let snapshot = collector.snapshot().await;
        assert_eq!(snapshot.cdp_commands.total_commands, 2);

        let summary = collector.summary().await;
        assert!(summary.cdp_avg_time.is_some());

        collector.reset().await;
        let snapshot = collector.snapshot().await;
        assert_eq!(snapshot.cdp_commands.total_commands, 0);
    }
}