use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct Metrics {
pub cdp_commands: CdpMetrics,
pub dom_processing: DomMetrics,
pub browser_ops: BrowserMetrics,
}
#[derive(Debug, Clone, Default)]
pub struct CdpMetrics {
pub total_commands: u64,
pub total_time: Duration,
pub successful_commands: u64,
pub failed_commands: u64,
pub command_times: HashMap<String, CommandStats>,
}
#[derive(Debug, Clone, Default)]
pub struct CommandStats {
pub count: u64,
pub total_time: Duration,
pub min_time: Duration,
pub max_time: Duration,
}
#[derive(Debug, Clone, Default)]
pub struct DomMetrics {
pub total_processed: u64,
pub total_time: Duration,
pub total_elements: u64,
pub interactive_elements: u64,
}
#[derive(Debug, Clone, Default)]
pub struct BrowserMetrics {
pub navigations: u64,
pub navigation_time: Duration,
pub screenshots: u64,
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 {
pub fn new() -> Self {
Default::default()
}
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);
}
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;
}
pub fn record_navigation(&mut self, duration: Duration) {
self.browser_ops.navigations += 1;
self.browser_ops.navigation_time += duration;
}
pub fn record_screenshot(&mut self, duration: Duration) {
self.browser_ops.screenshots += 1;
self.browser_ops.screenshot_time += duration;
}
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
}
}
}
#[derive(Debug, Clone)]
pub struct MetricsSummary {
pub cdp_avg_time: Option<Duration>,
pub cdp_success_rate: Option<f64>,
pub dom_avg_time: Option<Duration>,
pub dom_avg_elements: Option<f64>,
pub nav_avg_time: Option<Duration>,
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(())
}
}
#[derive(Debug, Clone)]
pub struct MetricsCollector {
metrics: Arc<RwLock<Metrics>>,
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
metrics: Arc::new(RwLock::new(Metrics::new())),
}
}
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);
}
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);
}
pub async fn record_navigation(&self, duration: Duration) {
let mut metrics = self.metrics.write().await;
metrics.record_navigation(duration);
}
pub async fn record_screenshot(&self, duration: Duration) {
let mut metrics = self.metrics.write().await;
metrics.record_screenshot(duration);
}
pub async fn snapshot(&self) -> Metrics {
self.metrics.read().await.clone()
}
pub async fn summary(&self) -> MetricsSummary {
self.metrics.read().await.summary()
}
pub async fn reset(&self) {
*self.metrics.write().await = Metrics::new();
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
pub struct Timer {
start: Instant,
}
impl Timer {
pub fn start() -> Self {
Self {
start: Instant::now(),
}
}
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);
}
}