blots_core/
stats.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3
4#[derive(Debug, Clone)]
5pub struct FunctionCallStats {
6    pub name: String,
7    pub start: std::time::Instant,
8    pub end: std::time::Instant,
9    pub start_var_env: Option<std::time::Instant>,
10    pub end_var_env: Option<std::time::Instant>,
11}
12
13impl FunctionCallStats {
14    pub fn total_duration_ms(&self) -> f64 {
15        (self.end - self.start).as_secs_f64() * 1_000.0
16    }
17
18    pub fn var_env_duration_ms(&self) -> Option<f64> {
19        self.start_var_env
20            .and_then(|start| self.end_var_env.map(|end| (end - start).as_secs_f64() * 1_000.0))
21    }
22
23    pub fn body_duration_ms(&self) -> f64 {
24        let var_env = self.var_env_duration_ms().unwrap_or(0.0);
25        self.total_duration_ms() - var_env
26    }
27}
28
29impl Display for FunctionCallStats {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        if self.start_var_env.is_some() && self.end_var_env.is_some() {
32            write!(
33                f,
34                "FunctionCallStats {{ name: {}, duration: {}ms, var_env_duration: {}ms }}",
35                self.name,
36                self.total_duration_ms(),
37                self.var_env_duration_ms().unwrap()
38            )
39        } else {
40            write!(
41                f,
42                "FunctionCallStats {{ name: {}, duration: {}ms, var_env_duration: None (built-in) }}",
43                self.name,
44                self.total_duration_ms()
45            )
46        }
47    }
48}
49
50#[derive(Debug, Default)]
51pub struct ProfilingSummary {
52    pub total_calls: usize,
53    pub lambda_calls: usize,
54    pub builtin_calls: usize,
55    pub total_time_ms: f64,
56    pub var_env_time_ms: f64,
57    pub body_time_ms: f64,
58    pub calls_by_name: HashMap<String, CallStats>,
59}
60
61#[derive(Debug, Clone)]
62pub struct CallStats {
63    pub count: usize,
64    pub total_ms: f64,
65    pub var_env_ms: f64,
66    pub body_ms: f64,
67    pub avg_ms: f64,
68    pub min_ms: f64,
69    pub max_ms: f64,
70}
71
72impl ProfilingSummary {
73    pub fn from_stats(stats: &[FunctionCallStats]) -> Self {
74        let mut summary = ProfilingSummary::default();
75        summary.total_calls = stats.len();
76
77        let mut calls_by_name: HashMap<String, Vec<&FunctionCallStats>> = HashMap::new();
78        for stat in stats {
79            calls_by_name
80                .entry(stat.name.clone())
81                .or_insert_with(Vec::new)
82                .push(stat);
83        }
84
85        for (name, calls) in calls_by_name.iter() {
86            let is_lambda = calls.iter().any(|c| c.var_env_duration_ms().is_some());
87            if is_lambda {
88                summary.lambda_calls += calls.len();
89            } else {
90                summary.builtin_calls += calls.len();
91            }
92
93            let mut total_ms = 0.0_f64;
94            let mut var_env_ms = 0.0_f64;
95            let mut body_ms = 0.0_f64;
96            let mut min_ms = f64::INFINITY;
97            let mut max_ms = 0.0_f64;
98
99            for call in calls.iter() {
100                let call_total = call.total_duration_ms();
101                let call_var_env = call.var_env_duration_ms().unwrap_or(0.0);
102                let call_body = call.body_duration_ms();
103
104                total_ms += call_total;
105                var_env_ms += call_var_env;
106                body_ms += call_body;
107                min_ms = min_ms.min(call_total);
108                max_ms = max_ms.max(call_total);
109            }
110
111            let count = calls.len();
112            summary.calls_by_name.insert(
113                name.clone(),
114                CallStats {
115                    count,
116                    total_ms,
117                    var_env_ms,
118                    body_ms,
119                    avg_ms: total_ms / count as f64,
120                    min_ms,
121                    max_ms,
122                },
123            );
124        }
125
126        // Calculate totals
127        for stats in summary.calls_by_name.values() {
128            summary.total_time_ms += stats.total_ms;
129            summary.var_env_time_ms += stats.var_env_ms;
130            summary.body_time_ms += stats.body_ms;
131        }
132
133        summary
134    }
135
136    pub fn print_summary(&self) {
137        println!("\n=== Performance Profile ===");
138        println!("Total function calls: {}", self.total_calls);
139        println!("  Lambda calls: {}", self.lambda_calls);
140        println!("  Built-in calls: {}", self.builtin_calls);
141        println!();
142        println!("Total time: {:.3}ms", self.total_time_ms);
143        println!("  Variable environment setup: {:.3}ms ({:.1}%)", 
144            self.var_env_time_ms,
145            if self.total_time_ms > 0.0 {
146                (self.var_env_time_ms / self.total_time_ms) * 100.0
147            } else {
148                0.0
149            }
150        );
151        println!("  Function body execution: {:.3}ms ({:.1}%)",
152            self.body_time_ms,
153            if self.total_time_ms > 0.0 {
154                (self.body_time_ms / self.total_time_ms) * 100.0
155            } else {
156                0.0
157            }
158        );
159        println!();
160
161        if !self.calls_by_name.is_empty() {
162            println!("Top functions by total time:");
163            let mut sorted: Vec<_> = self.calls_by_name.iter().collect();
164            sorted.sort_by(|a, b| b.1.total_ms.partial_cmp(&a.1.total_ms).unwrap());
165            
166            for (name, stats) in sorted.iter().take(10) {
167                let var_env_pct = if stats.total_ms > 0.0 {
168                    (stats.var_env_ms / stats.total_ms) * 100.0
169                } else {
170                    0.0
171                };
172                println!(
173                    "  {}: {} calls, {:.3}ms total (avg {:.3}ms, min {:.3}ms, max {:.3}ms)",
174                    name, stats.count, stats.total_ms, stats.avg_ms, stats.min_ms, stats.max_ms
175                );
176                if stats.var_env_ms > 0.0 {
177                    println!(
178                        "    └─ var_env: {:.3}ms ({:.1}%), body: {:.3}ms ({:.1}%)",
179                        stats.var_env_ms, var_env_pct,
180                        stats.body_ms, 100.0 - var_env_pct
181                    );
182                }
183            }
184        }
185    }
186}