cloudscraper_rs/modules/performance/
mod.rs1use std::collections::{HashMap, VecDeque};
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
10pub struct PerformanceConfig {
11 pub window: usize,
12 pub latency_threshold: Duration,
13 pub error_rate_threshold: f64,
14 pub min_samples: usize,
15}
16
17impl Default for PerformanceConfig {
18 fn default() -> Self {
19 Self {
20 window: 100,
21 latency_threshold: Duration::from_secs_f32(4.0),
22 error_rate_threshold: 0.25,
23 min_samples: 10,
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
29pub struct PerformanceReport {
30 pub global_latency: Option<Duration>,
31 pub slow_domains: Vec<(String, Duration)>,
32 pub error_domains: Vec<(String, f64)>,
33 pub alerts: Vec<String>,
34}
35
36impl PerformanceReport {
37 fn empty() -> Self {
38 Self {
39 global_latency: None,
40 slow_domains: Vec::new(),
41 error_domains: Vec::new(),
42 alerts: Vec::new(),
43 }
44 }
45}
46
47#[derive(Debug)]
48struct DomainPerformance {
49 latencies: VecDeque<Duration>,
50 successes: usize,
51 failures: usize,
52 window: usize,
53}
54
55impl DomainPerformance {
56 fn new(window: usize) -> Self {
57 Self {
58 latencies: VecDeque::with_capacity(window),
59 successes: 0,
60 failures: 0,
61 window,
62 }
63 }
64
65 fn record(&mut self, latency: Duration, success: bool) {
66 if self.latencies.len() == self.window {
67 self.latencies.pop_front();
68 }
69 self.latencies.push_back(latency);
70 if success {
71 self.successes += 1;
72 } else {
73 self.failures += 1;
74 }
75 }
76
77 fn average_latency(&self) -> Option<Duration> {
78 if self.latencies.is_empty() {
79 return None;
80 }
81 let total = self.latencies.iter().map(|d| d.as_secs_f64()).sum::<f64>();
82 Some(Duration::from_secs_f64(total / self.latencies.len() as f64))
83 }
84
85 fn error_rate(&self) -> Option<f64> {
86 let total = self.successes + self.failures;
87 if total == 0 {
88 return None;
89 }
90 Some(self.failures as f64 / total as f64)
91 }
92}
93
94#[derive(Debug)]
96pub struct PerformanceMonitor {
97 config: PerformanceConfig,
98 domains: HashMap<String, DomainPerformance>,
99 global_latencies: VecDeque<Duration>,
100}
101
102impl PerformanceMonitor {
103 pub fn new(config: PerformanceConfig) -> Self {
104 Self {
105 global_latencies: VecDeque::with_capacity(config.window),
106 domains: HashMap::new(),
107 config,
108 }
109 }
110
111 fn domain_mut(&mut self, domain: &str) -> &mut DomainPerformance {
112 self.domains
113 .entry(domain.to_string())
114 .or_insert_with(|| DomainPerformance::new(self.config.window))
115 }
116
117 pub fn record(
119 &mut self,
120 domain: &str,
121 latency: Duration,
122 success: bool,
123 ) -> Option<PerformanceReport> {
124 if self.global_latencies.len() == self.config.window {
125 self.global_latencies.pop_front();
126 }
127 self.global_latencies.push_back(latency);
128
129 let domain_state = self.domain_mut(domain);
130 domain_state.record(latency, success);
131
132 let should_report = domain_state.latencies.len() >= self.config.min_samples
133 || self.global_latencies.len() >= self.config.min_samples;
134 if !should_report {
135 return None;
136 }
137
138 let mut report = PerformanceReport::empty();
139 report.global_latency = self.global_latency();
140
141 for (domain_name, perf) in &self.domains {
142 if let Some(avg) = perf.average_latency()
143 && avg > self.config.latency_threshold
144 {
145 report.slow_domains.push((domain_name.clone(), avg));
146 }
147
148 if let Some(error_rate) = perf.error_rate()
149 && error_rate >= self.config.error_rate_threshold
150 {
151 report.error_domains.push((domain_name.clone(), error_rate));
152 }
153 }
154
155 if let Some(global) = report.global_latency
156 && global > self.config.latency_threshold
157 {
158 report.alerts.push(format!(
159 "Global latency {:.2}s exceeded threshold {:.2}s",
160 global.as_secs_f64(),
161 self.config.latency_threshold.as_secs_f64()
162 ));
163 }
164
165 for (domain, latency) in &report.slow_domains {
166 report.alerts.push(format!(
167 "Domain {} average latency {:.2}s exceeds threshold",
168 domain,
169 latency.as_secs_f64()
170 ));
171 }
172
173 for (domain, rate) in &report.error_domains {
174 report.alerts.push(format!(
175 "Domain {} error rate {:.1}% exceeds threshold",
176 domain,
177 rate * 100.0
178 ));
179 }
180
181 Some(report)
182 }
183
184 pub fn snapshot(&self) -> PerformanceReport {
185 let mut report = PerformanceReport::empty();
186 report.global_latency = self.global_latency();
187 for (domain, perf) in &self.domains {
188 if let Some(avg) = perf.average_latency()
189 && avg > self.config.latency_threshold
190 {
191 report.slow_domains.push((domain.clone(), avg));
192 }
193 if let Some(rate) = perf.error_rate()
194 && rate >= self.config.error_rate_threshold
195 {
196 report.error_domains.push((domain.clone(), rate));
197 }
198 }
199 report
200 }
201
202 fn global_latency(&self) -> Option<Duration> {
203 if self.global_latencies.is_empty() {
204 return None;
205 }
206 let total = self
207 .global_latencies
208 .iter()
209 .map(|d| d.as_secs_f64())
210 .sum::<f64>();
211 Some(Duration::from_secs_f64(
212 total / self.global_latencies.len() as f64,
213 ))
214 }
215}
216
217impl Default for PerformanceMonitor {
218 fn default() -> Self {
219 Self::new(PerformanceConfig::default())
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn emits_alert_for_high_latency() {
229 let mut monitor = PerformanceMonitor::new(PerformanceConfig {
230 latency_threshold: Duration::from_millis(200),
231 min_samples: 3,
232 ..Default::default()
233 });
234 for _ in 0..3 {
235 monitor.record("example.com", Duration::from_millis(500), true);
236 }
237 let report = monitor.snapshot();
238 assert!(!report.slow_domains.is_empty());
239 }
240}