1use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
14use std::sync::{Mutex, OnceLock};
15use std::time::{Duration, Instant};
16
17pub struct Profiler {
27 pub start_time: OnceLock<Instant>,
28 pub kernel_load_duration: Mutex<Option<Duration>>,
29 pub plugins_load_duration: Mutex<Option<Duration>>,
30 pub total_http_nanos: AtomicU64,
32 pub http_requests_count: AtomicUsize,
34 pub http_retries_count: AtomicUsize,
36 pub enabled: AtomicBool,
37}
38
39impl Profiler {
40 const fn new() -> Self {
41 Self {
42 start_time: OnceLock::new(),
43 kernel_load_duration: Mutex::new(None),
44 plugins_load_duration: Mutex::new(None),
45 total_http_nanos: AtomicU64::new(0),
46 http_requests_count: AtomicUsize::new(0),
47 http_retries_count: AtomicUsize::new(0),
48 enabled: AtomicBool::new(false),
49 }
50 }
51}
52
53static GLOBAL_PROFILER: Profiler = Profiler::new();
54
55pub fn init() {
57 let _ = GLOBAL_PROFILER.start_time.set(Instant::now());
58}
59
60pub fn enable() {
62 GLOBAL_PROFILER.enabled.store(true, Ordering::Relaxed);
63}
64
65pub fn is_enabled() -> bool {
67 GLOBAL_PROFILER.enabled.load(Ordering::Relaxed)
68}
69
70pub fn mark_kernel_loaded() {
72 if !is_enabled() {
73 return;
74 }
75 if let Some(start) = GLOBAL_PROFILER.start_time.get()
76 && let Ok(mut kernel_dur) = GLOBAL_PROFILER.kernel_load_duration.lock()
77 && kernel_dur.is_none()
78 {
79 *kernel_dur = Some(start.elapsed());
80 }
81}
82
83pub fn mark_plugins_loaded(duration: Duration) {
85 if !is_enabled() {
86 return;
87 }
88 if let Ok(mut plugins_dur) = GLOBAL_PROFILER.plugins_load_duration.lock()
89 && plugins_dur.is_none()
90 {
91 *plugins_dur = Some(duration);
92 }
93}
94
95pub fn record_http_request(duration: Duration) {
98 if !is_enabled() {
99 return;
100 }
101 GLOBAL_PROFILER
102 .total_http_nanos
103 .fetch_add(duration.as_nanos() as u64, Ordering::Relaxed);
104 GLOBAL_PROFILER
105 .http_requests_count
106 .fetch_add(1, Ordering::Relaxed);
107}
108
109pub fn record_http_retry() {
111 if !is_enabled() {
112 return;
113 }
114 GLOBAL_PROFILER
115 .http_retries_count
116 .fetch_add(1, Ordering::Relaxed);
117}
118
119pub fn report(command: Option<&str>, exit_code: Option<i32>) {
122 if !is_enabled() {
123 return;
124 }
125
126 let start_time = match GLOBAL_PROFILER.start_time.get() {
127 Some(t) => t,
128 None => return, };
130
131 let total_time = start_time.elapsed();
132 let kernel_time = GLOBAL_PROFILER
133 .kernel_load_duration
134 .lock()
135 .unwrap_or_else(|e| e.into_inner())
136 .unwrap_or_default();
137 let plugins_time_opt = *GLOBAL_PROFILER
138 .plugins_load_duration
139 .lock()
140 .unwrap_or_else(|e| e.into_inner());
141 let network_nanos = GLOBAL_PROFILER.total_http_nanos.load(Ordering::Relaxed);
142 let network_time = Duration::from_nanos(network_nanos);
143 let req_count = GLOBAL_PROFILER.http_requests_count.load(Ordering::Relaxed);
144 let retry_count = GLOBAL_PROFILER.http_retries_count.load(Ordering::Relaxed);
145
146 let other_time = total_time.saturating_sub(network_time);
147
148 let memory_str = if let Some(usage) = memory_stats::memory_stats() {
150 format!("{:.2} MB", usage.physical_mem as f64 / 1024.0 / 1024.0)
151 } else {
152 "Unknown".to_string()
153 };
154
155 let cmd = command.unwrap_or("unknown");
156 let code = exit_code.unwrap_or(0);
157
158 tracing::info!(
160 command = cmd,
161 exit_code = code,
162 total_ms = total_time.as_millis() as u64,
163 kernel_ms = kernel_time.as_millis() as u64,
164 plugins_ms = plugins_time_opt.map(|d| d.as_millis() as u64),
165 network_ms = network_time.as_millis() as u64,
166 other_ms = other_time.as_millis() as u64,
167 http_requests = req_count,
168 http_retries = retry_count,
169 memory = %memory_str,
170 "Performance profile"
171 );
172
173 let plugins_str = match plugins_time_opt {
175 Some(d) => format!("{:.3}s", d.as_secs_f64()),
176 None => "N/A".to_string(),
177 };
178
179 eprintln!("\n=== RAPS Performance Profile ===");
180 eprintln!(
181 "{:<22} {:.3}s",
182 "Total Execution Time:",
183 total_time.as_secs_f64()
184 );
185 eprintln!(
186 "{:<22} {:.3}s",
187 "Kernel Load Time:",
188 kernel_time.as_secs_f64()
189 );
190 eprintln!("{:<22} {}", "Plugins Load Time:", plugins_str);
191 eprintln!(
192 "{:<22} {:.3}s",
193 "Total Network Time:",
194 network_time.as_secs_f64()
195 );
196 eprintln!("{:<22} {}", "HTTP Requests:", req_count);
197 if retry_count > 0 {
198 eprintln!("{:<22} {}", "HTTP Retries:", retry_count);
199 }
200 let health_snap = crate::api_health::snapshot();
201 if health_snap.sample_count > 0 {
202 eprintln!(
203 "{:<22} {}",
204 "Avg Latency:",
205 crate::api_health::format_duration_ms(health_snap.avg_latency)
206 );
207 eprintln!(
208 "{:<22} {}",
209 "Latency Jitter:",
210 crate::api_health::format_duration_ms(health_snap.jitter)
211 );
212 eprintln!("{:<22} {}", "API Health:", health_snap.health_status);
213 }
214 eprintln!("{:<22} {:.3}s", "Other Time:", other_time.as_secs_f64());
215 eprintln!("{:<22} {}", "Memory (at exit):", memory_str);
216 eprintln!("================================");
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
228 fn test_atomic_http_duration_accumulates() {
229 let counter = AtomicU64::new(0);
230 let d1 = Duration::from_millis(100);
231 let d2 = Duration::from_millis(250);
232 counter.fetch_add(d1.as_nanos() as u64, Ordering::Relaxed);
233 counter.fetch_add(d2.as_nanos() as u64, Ordering::Relaxed);
234 let total = Duration::from_nanos(counter.load(Ordering::Relaxed));
235 assert_eq!(total, Duration::from_millis(350));
236 }
237
238 #[test]
239 fn test_atomic_request_count() {
240 let counter = AtomicUsize::new(0);
241 counter.fetch_add(1, Ordering::Relaxed);
242 counter.fetch_add(1, Ordering::Relaxed);
243 counter.fetch_add(1, Ordering::Relaxed);
244 assert_eq!(counter.load(Ordering::Relaxed), 3);
245 }
246
247 #[test]
248 fn test_mark_plugins_loaded_only_once() {
249 let plugins_dur: Mutex<Option<Duration>> = Mutex::new(None);
250
251 {
253 let mut dur = plugins_dur.lock().unwrap();
254 if dur.is_none() {
255 *dur = Some(Duration::from_millis(50));
256 }
257 }
258
259 {
261 let mut dur = plugins_dur.lock().unwrap();
262 if dur.is_none() {
263 *dur = Some(Duration::from_millis(200));
264 }
265 }
266
267 let result = plugins_dur.lock().unwrap().unwrap();
268 assert_eq!(result, Duration::from_millis(50));
269 }
270
271 #[test]
272 fn test_mark_kernel_loaded_only_once() {
273 let kernel_dur: Mutex<Option<Duration>> = Mutex::new(None);
274
275 {
276 let mut dur = kernel_dur.lock().unwrap();
277 if dur.is_none() {
278 *dur = Some(Duration::from_millis(30));
279 }
280 }
281
282 {
283 let mut dur = kernel_dur.lock().unwrap();
284 if dur.is_none() {
285 *dur = Some(Duration::from_millis(100));
286 }
287 }
288
289 let result = kernel_dur.lock().unwrap().unwrap();
290 assert_eq!(result, Duration::from_millis(30));
291 }
292
293 #[test]
294 fn test_plugins_none_displays_as_na() {
295 let plugins_time_opt: Option<Duration> = None;
296 let plugins_str = match plugins_time_opt {
297 Some(d) => format!("{:.3}s", d.as_secs_f64()),
298 None => "N/A".to_string(),
299 };
300 assert_eq!(plugins_str, "N/A");
301 }
302
303 #[test]
304 fn test_plugins_some_displays_as_seconds() {
305 let plugins_time_opt: Option<Duration> = Some(Duration::from_millis(123));
306 let plugins_str = match plugins_time_opt {
307 Some(d) => format!("{:.3}s", d.as_secs_f64()),
308 None => "N/A".to_string(),
309 };
310 assert_eq!(plugins_str, "0.123s");
311 }
312
313 #[test]
314 fn test_other_time_saturating_sub() {
315 let total = Duration::from_millis(500);
316 let network = Duration::from_millis(300);
317 let other = total.saturating_sub(network);
318 assert_eq!(other, Duration::from_millis(200));
319 }
320
321 #[test]
322 fn test_other_time_saturating_sub_overflow() {
323 let total = Duration::from_millis(100);
325 let network = Duration::from_millis(300);
326 let other = total.saturating_sub(network);
327 assert_eq!(other, Duration::ZERO);
328 }
329
330 #[test]
331 fn test_enabled_flag() {
332 let enabled = AtomicBool::new(false);
333 assert!(!enabled.load(Ordering::Relaxed));
334 enabled.store(true, Ordering::Relaxed);
335 assert!(enabled.load(Ordering::Relaxed));
336 }
337}