browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Chrome DevTools tracing / CPU profiling via CDP Tracing domain

use crate::browser::views::{TraceConfig, TraceResult};
use crate::error::Result;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{Duration, Instant};

/// Manages Chrome DevTools tracing for performance analysis
#[derive(Debug, Clone)]
pub struct TracingManager {
    client: Arc<crate::browser::cdp::CdpClient>,
    events: Arc<Mutex<Vec<serde_json::Value>>>,
    start_time: Arc<Mutex<Option<Instant>>>,
}

impl TracingManager {
    /// Create a new tracing manager
    pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
        Self {
            client,
            events: Arc::new(Mutex::new(Vec::new())),
            start_time: Arc::new(Mutex::new(None)),
        }
    }

    /// Start collecting trace events.
    /// Default categories include devtools.timeline for CPU/network profiling.
    pub async fn start(&self, config: Option<TraceConfig>) -> Result<()> {
        let config = config.unwrap_or_else(|| TraceConfig {
            categories: vec![
                "devtools.timeline".to_string(),
                "disabled-by-default-devtools.timeline".to_string(),
                "v8.execute".to_string(),
                "disabled-by-default-v8.cpu_profiler".to_string(),
            ],
            streaming: Some(true),
            buffer_size: None,
        });

        let mut params = serde_json::json!({
            "categories": config.categories.join(","),
            "transferMode": "ReturnAsStream",
        });
        if let Some(true) = config.streaming {
            params["transferMode"] = serde_json::json!("ReturnAsStream");
        }
        if let Some(size) = config.buffer_size {
            params["bufferUsageReportingInterval"] = serde_json::json!(size);
        }

        self.client
            .send_command("Tracing.start", params)
            .await?;

        *self.start_time.lock().await = Some(Instant::now());

        let events = Arc::clone(&self.events);
        let mut rx = self.client.subscribe_events().await?;

        tokio::spawn(async move {
            while let Ok(event) = rx.recv().await {
                match event.method.as_str() {
                    "Tracing.dataCollected" => {
                        if let Some(value) = event.params.get("value")
                            && let Some(arr) = value.as_array() {
                                events.lock().await.extend(arr.iter().cloned());
                            }
                    }
                    "Tracing.tracingComplete" => break,
                    _ => {}
                }
            }
        });

        Ok(())
    }

    /// Stop tracing and return the collected events.
    /// After calling end(), the trace data is assembled into a TraceResult.
    pub async fn end(&self) -> Result<TraceResult> {
        let start = self.start_time.lock().await.take();
        let elapsed = start.map(|s| s.elapsed().as_secs_f64());

        self.client
            .send_command("Tracing.end", serde_json::json!({}))
            .await?;

        // Wait a brief moment for Tracing.tracingComplete event to propagate
        tokio::time::sleep(Duration::from_millis(200)).await;

        let events = self.events.lock().await.clone();

        Ok(TraceResult {
            success: !events.is_empty(),
            events,
            start_time: None,
            end_time: elapsed,
        })
    }

    /// Export trace events to a JSON file in Chrome DevTools Timeline format.
    /// The output can be loaded into Chrome DevTools Performance panel.
    pub async fn export_trace(&self, path: &std::path::Path, result: &TraceResult) -> Result<()> {
        let trace_json = serde_json::json!(result.events);
        let content = serde_json::to_string_pretty(&trace_json)?;
        tokio::fs::write(path, content).await?;
        Ok(())
    }

    /// Get the number of collected events without stopping the trace
    pub async fn event_count(&self) -> usize {
        self.events.lock().await.len()
    }

    /// Clear collected events (useful between trace sessions)
    pub async fn clear(&self) {
        self.events.lock().await.clear();
    }
}

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

    #[test]
    fn test_trace_config_default() {
        let config = TraceConfig::default();
        assert!(config.categories.is_empty());
        assert_eq!(config.streaming, None);
        assert_eq!(config.buffer_size, None);
    }

    #[test]
    fn test_trace_config_custom() {
        let config = TraceConfig {
            categories: vec!["devtools.timeline".to_string()],
            streaming: Some(true),
            buffer_size: Some(1024 * 1024),
        };
        assert_eq!(config.categories.len(), 1);
        assert!(config.streaming.unwrap());
    }

    #[test]
    fn test_trace_result_creation() {
        let result = TraceResult {
            success: true,
            events: vec![],
            start_time: None,
            end_time: Some(1.5),
        };
        assert!(result.success);
        assert_eq!(result.end_time, Some(1.5));
    }

    #[test]
    fn test_trace_result_serialization() {
        let result = TraceResult {
            success: true,
            events: vec![serde_json::json!({"name": "test"})],
            start_time: None,
            end_time: Some(0.5),
        };
        let json = serde_json::to_string(&result).unwrap();
        assert!(json.contains("test"));
    }
}