Skip to main content

profile_inspect/output/
collapsed.rs

1use std::collections::HashMap;
2use std::io::Write;
3
4use crate::analysis::{CpuAnalysis, HeapAnalysis};
5use crate::ir::ProfileIR;
6
7use super::{Formatter, OutputError};
8
9/// Collapsed stacks formatter for flamegraph tools
10///
11/// Output format (Brendan Gregg's folded format):
12/// ```text
13/// main;foo;bar 42
14/// main;foo;baz 17
15/// ```
16///
17/// Compatible with:
18/// - Brendan Gregg's flamegraph.pl
19/// - inferno (Rust flamegraph tool)
20/// - speedscope (can import collapsed stacks)
21pub struct CollapsedFormatter;
22
23impl Formatter for CollapsedFormatter {
24    fn write_cpu_analysis(
25        &self,
26        profile: &ProfileIR,
27        _analysis: &CpuAnalysis,
28        writer: &mut dyn Write,
29    ) -> Result<(), OutputError> {
30        // Aggregate stacks
31        let mut stack_weights: HashMap<String, u64> = HashMap::new();
32
33        for sample in &profile.samples {
34            if let Some(stack) = profile.get_stack(sample.stack_id) {
35                // Build collapsed stack string
36                let stack_str: String = stack
37                    .frames
38                    .iter()
39                    .filter_map(|fid| {
40                        profile.get_frame(*fid).map(|f| {
41                            // Clean up function name for collapsed format
42                            let name = if f.name.is_empty() {
43                                "(anonymous)".to_string()
44                            } else {
45                                // Replace semicolons which are used as delimiters
46                                f.name.replace(';', ":")
47                            };
48
49                            // Optionally include file info
50                            if let Some(ref file) = f.file {
51                                // Extract just filename
52                                let filename = file.rsplit('/').next().unwrap_or(file);
53                                if let Some(line) = f.line {
54                                    format!("{name} ({filename}:{line})")
55                                } else {
56                                    format!("{name} ({filename})")
57                                }
58                            } else {
59                                name
60                            }
61                        })
62                    })
63                    .collect::<Vec<_>>()
64                    .join(";");
65
66                if !stack_str.is_empty() {
67                    *stack_weights.entry(stack_str).or_default() += sample.weight;
68                }
69            }
70        }
71
72        // Sort by stack name for deterministic output
73        let mut stacks: Vec<_> = stack_weights.into_iter().collect();
74        stacks.sort_by(|a, b| a.0.cmp(&b.0));
75
76        // Write collapsed stacks
77        for (stack, weight) in stacks {
78            writeln!(writer, "{stack} {weight}")?;
79        }
80
81        Ok(())
82    }
83
84    fn write_heap_analysis(
85        &self,
86        profile: &ProfileIR,
87        _analysis: &HeapAnalysis,
88        writer: &mut dyn Write,
89    ) -> Result<(), OutputError> {
90        // Same format but with allocation sizes instead of time
91        let mut stack_weights: HashMap<String, u64> = HashMap::new();
92
93        for sample in &profile.samples {
94            if let Some(stack) = profile.get_stack(sample.stack_id) {
95                let stack_str: String = stack
96                    .frames
97                    .iter()
98                    .filter_map(|fid| {
99                        profile.get_frame(*fid).map(|f| {
100                            let name = if f.name.is_empty() {
101                                "(anonymous)".to_string()
102                            } else {
103                                f.name.replace(';', ":")
104                            };
105
106                            if let Some(ref file) = f.file {
107                                let filename = file.rsplit('/').next().unwrap_or(file);
108                                if let Some(line) = f.line {
109                                    format!("{name} ({filename}:{line})")
110                                } else {
111                                    format!("{name} ({filename})")
112                                }
113                            } else {
114                                name
115                            }
116                        })
117                    })
118                    .collect::<Vec<_>>()
119                    .join(";");
120
121                if !stack_str.is_empty() {
122                    *stack_weights.entry(stack_str).or_default() += sample.weight;
123                }
124            }
125        }
126
127        let mut stacks: Vec<_> = stack_weights.into_iter().collect();
128        stacks.sort_by(|a, b| a.0.cmp(&b.0));
129
130        for (stack, weight) in stacks {
131            writeln!(writer, "{stack} {weight}")?;
132        }
133
134        Ok(())
135    }
136}