Skip to main content

profile_inspect/analysis/
caller_callee.rs

1use std::collections::HashMap;
2
3use crate::ir::{FrameId, ProfileIR};
4
5/// Statistics about a caller of a function
6#[derive(Debug, Clone)]
7pub struct CallerStats {
8    /// Frame ID of the caller
9    pub frame_id: FrameId,
10    /// Caller function name
11    pub name: String,
12    /// Caller source location
13    pub location: String,
14    /// Number of times this caller called the target function
15    pub call_count: u32,
16    /// Time attributed to calls from this caller (time in target when called from this caller)
17    pub time: u64,
18}
19
20/// Statistics about a callee of a function
21#[derive(Debug, Clone)]
22pub struct CalleeStats {
23    /// Frame ID of the callee
24    pub frame_id: FrameId,
25    /// Callee function name
26    pub name: String,
27    /// Callee source location
28    pub location: String,
29    /// Number of times the target function called this callee
30    pub call_count: u32,
31    /// Self time of this callee when called by target
32    pub self_time: u64,
33    /// Total inclusive time of this callee subtree when called by target
34    pub total_time: u64,
35}
36
37/// Caller/callee analysis for a specific function
38#[derive(Debug)]
39pub struct CallerCalleeAnalysis {
40    /// The target function being analyzed
41    pub target_frame_id: FrameId,
42    /// Target function name
43    pub target_name: String,
44    /// Target function location
45    pub target_location: String,
46    /// Self time of the target function
47    pub target_self_time: u64,
48    /// Total time of the target function
49    pub target_total_time: u64,
50    /// Functions that call this function
51    pub callers: Vec<CallerStats>,
52    /// Functions that this function calls
53    pub callees: Vec<CalleeStats>,
54}
55
56/// Analyzer for caller/callee relationships
57pub struct CallerCalleeAnalyzer {
58    /// Maximum number of callers/callees to return
59    top_n: usize,
60}
61
62impl CallerCalleeAnalyzer {
63    /// Create a new analyzer
64    pub fn new() -> Self {
65        Self { top_n: 20 }
66    }
67
68    /// Set maximum number of results
69    pub fn top_n(mut self, n: usize) -> Self {
70        self.top_n = n;
71        self
72    }
73
74    /// Analyze callers and callees for a specific frame
75    pub fn analyze(
76        &self,
77        profile: &ProfileIR,
78        target_frame_id: FrameId,
79    ) -> Option<CallerCalleeAnalysis> {
80        let target_frame = profile.get_frame(target_frame_id)?;
81
82        let mut caller_times: HashMap<FrameId, u64> = HashMap::new();
83        let mut caller_counts: HashMap<FrameId, u32> = HashMap::new();
84        let mut callee_self_times: HashMap<FrameId, u64> = HashMap::new();
85        let mut callee_total_times: HashMap<FrameId, u64> = HashMap::new();
86        let mut callee_counts: HashMap<FrameId, u32> = HashMap::new();
87        let mut target_self_time: u64 = 0;
88        let mut target_total_time: u64 = 0;
89
90        // Analyze caller/callee relationships
91        for sample in &profile.samples {
92            let weight = sample.weight;
93
94            if let Some(stack) = profile.get_stack(sample.stack_id) {
95                // Find target in the stack
96                let target_idx = stack.frames.iter().position(|&f| f == target_frame_id);
97
98                if let Some(idx) = target_idx {
99                    target_total_time += weight;
100
101                    // Check if target is the leaf (self time)
102                    if idx == stack.frames.len() - 1 {
103                        target_self_time += weight;
104                    }
105
106                    // Record caller (frame before target in stack)
107                    if idx > 0 {
108                        let caller_id = stack.frames[idx - 1];
109                        *caller_times.entry(caller_id).or_default() += weight;
110                        *caller_counts.entry(caller_id).or_default() += 1;
111                    }
112
113                    // Record callees (frames after target in stack)
114                    if idx < stack.frames.len() - 1 {
115                        // Direct callee
116                        let callee_id = stack.frames[idx + 1];
117                        *callee_counts.entry(callee_id).or_default() += 1;
118                        // Total time for this callee subtree (sample weight)
119                        *callee_total_times.entry(callee_id).or_default() += weight;
120
121                        // Self time: only when callee is the leaf
122                        if let Some(&leaf_id) = stack.frames.last() {
123                            if leaf_id == callee_id {
124                                *callee_self_times.entry(callee_id).or_default() += weight;
125                            }
126                        }
127                    }
128                }
129            }
130        }
131
132        // Build caller stats
133        let mut callers: Vec<CallerStats> = caller_times
134            .iter()
135            .filter_map(|(&fid, &time)| {
136                let frame = profile.get_frame(fid)?;
137                Some(CallerStats {
138                    frame_id: fid,
139                    name: frame.display_name().to_string(),
140                    location: frame.location(),
141                    call_count: caller_counts.get(&fid).copied().unwrap_or(0),
142                    time,
143                })
144            })
145            .collect();
146
147        callers.sort_by(|a, b| b.time.cmp(&a.time).then_with(|| a.name.cmp(&b.name)));
148        callers.truncate(self.top_n);
149
150        // Build callee stats
151        let mut callees: Vec<CalleeStats> = callee_total_times
152            .iter()
153            .filter_map(|(&fid, &total_time)| {
154                let frame = profile.get_frame(fid)?;
155                Some(CalleeStats {
156                    frame_id: fid,
157                    name: frame.display_name().to_string(),
158                    location: frame.location(),
159                    call_count: callee_counts.get(&fid).copied().unwrap_or(0),
160                    self_time: callee_self_times.get(&fid).copied().unwrap_or(0),
161                    total_time,
162                })
163            })
164            .collect();
165
166        callees.sort_by(|a, b| {
167            b.total_time
168                .cmp(&a.total_time)
169                .then_with(|| a.name.cmp(&b.name))
170        });
171        callees.truncate(self.top_n);
172
173        Some(CallerCalleeAnalysis {
174            target_frame_id,
175            target_name: target_frame.display_name().to_string(),
176            target_location: target_frame.location(),
177            target_self_time,
178            target_total_time,
179            callers,
180            callees,
181        })
182    }
183
184    /// Find a frame by function name (partial match)
185    pub fn find_frame_by_name<'a>(
186        profile: &'a ProfileIR,
187        name: &str,
188    ) -> Option<&'a crate::ir::Frame> {
189        profile
190            .frames
191            .iter()
192            .find(|f| f.name.contains(name) || f.display_name().contains(name))
193    }
194}
195
196impl Default for CallerCalleeAnalyzer {
197    fn default() -> Self {
198        Self::new()
199    }
200}