1use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FfiCallStats {
15 pub name: String,
17 pub call_count: u64,
19 pub total_duration: Duration,
21 pub min_duration: Duration,
23 pub max_duration: Duration,
25 pub avg_duration: Duration,
27}
28
29impl FfiCallStats {
30 fn new(name: String) -> Self {
31 Self {
32 name,
33 call_count: 0,
34 total_duration: Duration::ZERO,
35 min_duration: Duration::MAX,
36 max_duration: Duration::ZERO,
37 avg_duration: Duration::ZERO,
38 }
39 }
40
41 fn record(&mut self, duration: Duration) {
42 self.call_count += 1;
43 self.total_duration += duration;
44 self.min_duration = self.min_duration.min(duration);
45 self.max_duration = self.max_duration.max(duration);
46 self.avg_duration = self.total_duration / self.call_count as u32;
47 }
48
49 pub fn exceeds_target(&self, target_micros: u64) -> bool {
51 self.avg_duration.as_micros() > target_micros as u128
52 }
53
54 pub fn overhead_percentage(&self, target_micros: u64) -> f64 {
56 let avg_micros = self.avg_duration.as_micros() as f64;
57 ((avg_micros - target_micros as f64) / target_micros as f64) * 100.0
58 }
59}
60
61pub struct FfiProfiler {
63 stats: Arc<RwLock<HashMap<String, FfiCallStats>>>,
64 enabled: Arc<RwLock<bool>>,
65}
66
67impl FfiProfiler {
68 pub fn new() -> Self {
70 Self {
71 stats: Arc::new(RwLock::new(HashMap::new())),
72 enabled: Arc::new(RwLock::new(true)),
73 }
74 }
75
76 pub fn enable(&self) {
78 *self.enabled.write() = true;
79 }
80
81 pub fn disable(&self) {
83 *self.enabled.write() = false;
84 }
85
86 pub fn is_enabled(&self) -> bool {
88 *self.enabled.read()
89 }
90
91 pub fn start(&self, name: &str) -> FfiCallGuard {
93 FfiCallGuard {
94 name: name.to_string(),
95 start: Instant::now(),
96 profiler: self.clone(),
97 }
98 }
99
100 fn record(&self, name: String, duration: Duration) {
102 if !self.is_enabled() {
103 return;
104 }
105
106 let mut stats = self.stats.write();
107 stats
108 .entry(name.clone())
109 .or_insert_with(|| FfiCallStats::new(name))
110 .record(duration);
111 }
112
113 pub fn get_stats(&self, name: &str) -> Option<FfiCallStats> {
115 self.stats.read().get(name).cloned()
116 }
117
118 pub fn get_all_stats(&self) -> Vec<FfiCallStats> {
120 self.stats.read().values().cloned().collect()
121 }
122
123 pub fn reset(&self) {
125 self.stats.write().clear();
126 }
127
128 pub fn get_hotspots(&self) -> Vec<FfiCallStats> {
130 let mut stats = self.get_all_stats();
131 stats.sort_by_key(|s| std::cmp::Reverse(s.avg_duration));
132 stats
133 }
134
135 pub fn total_overhead(&self) -> Duration {
137 self.stats.read().values().map(|s| s.total_duration).sum()
138 }
139
140 pub fn report(&self) -> ProfilingReport {
142 let stats = self.get_all_stats();
143 let total_calls: u64 = stats.iter().map(|s| s.call_count).sum();
144 let total_duration = self.total_overhead();
145
146 ProfilingReport {
147 total_calls,
148 total_duration,
149 function_stats: stats,
150 }
151 }
152}
153
154impl Default for FfiProfiler {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl Clone for FfiProfiler {
161 fn clone(&self) -> Self {
162 Self {
163 stats: Arc::clone(&self.stats),
164 enabled: Arc::clone(&self.enabled),
165 }
166 }
167}
168
169pub struct FfiCallGuard {
171 name: String,
172 start: Instant,
173 profiler: FfiProfiler,
174}
175
176impl Drop for FfiCallGuard {
177 fn drop(&mut self) {
178 let duration = self.start.elapsed();
179 self.profiler.record(self.name.clone(), duration);
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ProfilingReport {
186 pub total_calls: u64,
188 pub total_duration: Duration,
190 pub function_stats: Vec<FfiCallStats>,
192}
193
194impl ProfilingReport {
195 pub fn print(&self) {
197 println!("\n=== FFI Profiling Report ===");
198 println!("Total calls: {}", self.total_calls);
199 println!("Total duration: {:?}", self.total_duration);
200 println!("\nFunction statistics:");
201 println!(
202 "{:<30} {:>10} {:>15} {:>15} {:>15}",
203 "Function", "Calls", "Avg (μs)", "Min (μs)", "Max (μs)"
204 );
205 println!("{}", "-".repeat(85));
206
207 let mut sorted_stats = self.function_stats.clone();
208 sorted_stats.sort_by_key(|s| std::cmp::Reverse(s.avg_duration));
209
210 for stat in sorted_stats {
211 println!(
212 "{:<30} {:>10} {:>15.2} {:>15.2} {:>15.2}",
213 stat.name,
214 stat.call_count,
215 stat.avg_duration.as_micros() as f64,
216 stat.min_duration.as_micros() as f64,
217 stat.max_duration.as_micros() as f64,
218 );
219 }
220 }
221
222 pub fn identify_bottlenecks(&self, target_micros: u64) -> Vec<String> {
224 self.function_stats
225 .iter()
226 .filter(|s| s.exceeds_target(target_micros))
227 .map(|s| s.name.clone())
228 .collect()
229 }
230
231 pub fn summary(&self) -> OverheadSummary {
233 let avg_call_duration = if self.total_calls > 0 {
234 self.total_duration / self.total_calls as u32
235 } else {
236 Duration::ZERO
237 };
238
239 let max_duration = self
240 .function_stats
241 .iter()
242 .map(|s| s.max_duration)
243 .max()
244 .unwrap_or(Duration::ZERO);
245
246 OverheadSummary {
247 total_calls: self.total_calls,
248 total_duration: self.total_duration,
249 avg_call_duration,
250 max_call_duration: max_duration,
251 functions_profiled: self.function_stats.len(),
252 }
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct OverheadSummary {
259 pub total_calls: u64,
260 pub total_duration: Duration,
261 pub avg_call_duration: Duration,
262 pub max_call_duration: Duration,
263 pub functions_profiled: usize,
264}
265
266impl OverheadSummary {
267 pub fn meets_target(&self, target_micros: u64) -> bool {
269 self.avg_call_duration.as_micros() <= target_micros as u128
270 }
271}
272
273static GLOBAL_PROFILER: once_cell::sync::Lazy<FfiProfiler> =
275 once_cell::sync::Lazy::new(FfiProfiler::new);
276
277pub fn global_profiler() -> &'static FfiProfiler {
279 &GLOBAL_PROFILER
280}
281
282#[macro_export]
284macro_rules! profile_ffi {
285 ($name:expr, $body:expr) => {{
286 let _guard = $crate::ffi_profiler::global_profiler().start($name);
287 $body
288 }};
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use std::thread;
295
296 #[test]
297 fn test_ffi_profiler_basic() {
298 let profiler = FfiProfiler::new();
299
300 {
302 let _guard = profiler.start("test_function");
303 thread::sleep(Duration::from_millis(10));
304 }
305
306 let stats = profiler.get_stats("test_function");
307 assert!(stats.is_some());
308
309 let stats = stats.expect("test: should succeed");
310 assert_eq!(stats.call_count, 1);
311 assert!(stats.avg_duration >= Duration::from_millis(10));
312 }
313
314 #[test]
315 fn test_multiple_calls() {
316 let profiler = FfiProfiler::new();
317
318 for _ in 0..5 {
319 let _guard = profiler.start("multi_call");
320 thread::sleep(Duration::from_millis(5));
321 }
322
323 let stats = profiler
324 .get_stats("multi_call")
325 .expect("test: should succeed");
326 assert_eq!(stats.call_count, 5);
327 assert!(stats.avg_duration >= Duration::from_millis(5));
328 }
329
330 #[test]
331 fn test_enable_disable() {
332 let profiler = FfiProfiler::new();
333
334 profiler.disable();
335 {
336 let _guard = profiler.start("disabled");
337 thread::sleep(Duration::from_millis(5));
338 }
339
340 assert!(profiler.get_stats("disabled").is_none());
341
342 profiler.enable();
343 {
344 let _guard = profiler.start("enabled");
345 thread::sleep(Duration::from_millis(5));
346 }
347
348 assert!(profiler.get_stats("enabled").is_some());
349 }
350
351 #[test]
352 fn test_reset() {
353 let profiler = FfiProfiler::new();
354
355 {
356 let _guard = profiler.start("test");
357 thread::sleep(Duration::from_millis(5));
358 }
359
360 assert!(profiler.get_stats("test").is_some());
361
362 profiler.reset();
363 assert!(profiler.get_stats("test").is_none());
364 }
365
366 #[test]
367 fn test_hotspots() {
368 let profiler = FfiProfiler::new();
369
370 {
371 let _guard = profiler.start("fast");
372 thread::sleep(Duration::from_millis(1));
373 }
374
375 {
376 let _guard = profiler.start("slow");
377 thread::sleep(Duration::from_millis(10));
378 }
379
380 let hotspots = profiler.get_hotspots();
381 assert_eq!(hotspots.len(), 2);
382 assert_eq!(hotspots[0].name, "slow");
383 assert_eq!(hotspots[1].name, "fast");
384 }
385
386 #[test]
387 fn test_profiling_report() {
388 let profiler = FfiProfiler::new();
389
390 for i in 0..3 {
391 let _guard = profiler.start(&format!("func_{}", i));
392 thread::sleep(Duration::from_millis(5));
393 }
394
395 let report = profiler.report();
396 assert_eq!(report.total_calls, 3);
397 assert_eq!(report.function_stats.len(), 3);
398
399 let summary = report.summary();
400 assert_eq!(summary.total_calls, 3);
401 assert_eq!(summary.functions_profiled, 3);
402 }
403
404 #[test]
405 fn test_exceeds_target() {
406 let mut stats = FfiCallStats::new("test".to_string());
407 stats.record(Duration::from_micros(500));
408
409 assert!(!stats.exceeds_target(1000));
410 assert!(stats.exceeds_target(100));
411 }
412
413 #[test]
414 fn test_identify_bottlenecks() {
415 let profiler = FfiProfiler::new();
416
417 {
420 let mut stats = profiler.stats.write();
421 let mut fast_stat = FfiCallStats::new("fast".to_string());
422 fast_stat.record(Duration::from_micros(100));
423 stats.insert("fast".to_string(), fast_stat);
424
425 let mut slow_stat = FfiCallStats::new("slow".to_string());
426 slow_stat.record(Duration::from_millis(10));
427 stats.insert("slow".to_string(), slow_stat);
428 }
429
430 let report = profiler.report();
431 let bottlenecks = report.identify_bottlenecks(1000);
433
434 assert!(bottlenecks.contains(&"slow".to_string()));
435 assert!(!bottlenecks.contains(&"fast".to_string()));
436 }
437
438 #[test]
439 fn test_global_profiler() {
440 let profiler = global_profiler();
441
442 profiler.reset(); {
445 let _guard = profiler.start("global_test");
446 thread::sleep(Duration::from_millis(5));
447 }
448
449 let stats = profiler.get_stats("global_test");
450 assert!(stats.is_some());
451 }
452}