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