pyroscope_rbspy_oncpu/ui/
summary.rs

1use anyhow::Result;
2use std::collections::{HashMap, HashSet};
3use std::io;
4
5use crate::core::types::StackFrame;
6
7struct Counts {
8    self_: u64,
9    total: u64,
10}
11
12pub struct Stats {
13    counts: HashMap<String, Counts>,
14    start_time: std::time::Instant,
15    total_traces: u32,
16}
17
18impl Stats {
19    const HEADER: &'static str = "% self  % total  name";
20
21    pub fn new() -> Stats {
22        Stats {
23            counts: HashMap::new(),
24            start_time: std::time::Instant::now(),
25            total_traces: 0,
26        }
27    }
28
29    fn inc_self(&mut self, name: String) {
30        let entry = self
31            .counts
32            .entry(name)
33            .or_insert(Counts { self_: 0, total: 0 });
34        entry.self_ += 1;
35    }
36
37    fn inc_tot(&mut self, name: String) {
38        let entry = self
39            .counts
40            .entry(name)
41            .or_insert(Counts { self_: 0, total: 0 });
42        entry.total += 1;
43    }
44
45    fn name_function(frame: &StackFrame) -> String {
46        let lineno = match frame.lineno {
47            Some(lineno) => format!(":{}", lineno),
48            None => "".to_string(),
49        };
50        format!("{} - {}{}", frame.name, frame.relative_path, lineno)
51    }
52
53    fn name_lineno(frame: &StackFrame) -> String {
54        format!("{}", frame)
55    }
56
57    // Aggregate by function name
58    pub fn add_function_name(&mut self, stack: &[StackFrame]) {
59        if stack.is_empty() {
60            return;
61        }
62        self.total_traces += 1;
63        self.inc_self(Stats::name_function(&stack[0]));
64        let mut set: HashSet<String> = HashSet::new();
65        for frame in stack {
66            set.insert(Stats::name_function(frame));
67        }
68        for name in set.into_iter() {
69            self.inc_tot(name);
70        }
71    }
72
73    // Aggregate by function name + line number
74    pub fn add_lineno(&mut self, stack: &[StackFrame]) {
75        if stack.is_empty() {
76            return;
77        }
78        self.total_traces += 1;
79        self.inc_self(Stats::name_lineno(&stack[0]));
80        let mut set: HashSet<&StackFrame> = HashSet::new();
81        for frame in stack {
82            set.insert(&frame);
83        }
84        for frame in set {
85            self.inc_tot(Stats::name_lineno(frame));
86        }
87    }
88
89    pub fn write(&self, w: &mut dyn io::Write) -> Result<()> {
90        self.write_counts(w, None, None)
91    }
92
93    pub fn write_top_n(
94        &self,
95        w: &mut dyn io::Write,
96        n: usize,
97        truncate: Option<usize>,
98    ) -> Result<()> {
99        self.write_counts(w, Some(n), truncate)
100    }
101
102    pub fn elapsed_time(&self) -> std::time::Duration {
103        std::time::Instant::now() - self.start_time
104    }
105
106    fn write_counts(
107        &self,
108        w: &mut dyn io::Write,
109        top: Option<usize>,
110        truncate: Option<usize>,
111    ) -> Result<()> {
112        let top = top.unwrap_or(::std::usize::MAX);
113        let truncate = truncate.unwrap_or(::std::usize::MAX);
114        let mut sorted: Vec<(u64, u64, &str)> = self
115            .counts
116            .iter()
117            .map(|(x, y)| (y.self_, y.total, x.as_ref()))
118            .collect();
119        sorted.sort_unstable();
120        let counts = sorted.iter().rev().take(top);
121        writeln!(w, "{}", Stats::HEADER)?;
122        for &(self_, total, name) in counts {
123            writeln!(
124                w,
125                "{:>6.2} {:>8.2}  {:.*}",
126                100.0 * (self_ as f64) / f64::from(self.total_traces),
127                100.0 * (total as f64) / f64::from(self.total_traces),
128                truncate - 14 - 3,
129                name
130            )?;
131        }
132        Ok(())
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use crate::ui::summary::*;
139
140    // Build a test stackframe
141    fn f(i: usize) -> StackFrame {
142        StackFrame {
143            name: format!("func{}", i),
144            relative_path: format!("file{}.rb", i),
145            absolute_path: None,
146            lineno: Some(i),
147        }
148    }
149
150    #[test]
151    fn stats_by_function() {
152        let mut stats = Stats::new();
153
154        stats.add_function_name(&vec![f(1)]);
155        stats.add_function_name(&vec![f(3), f(2), f(1)]);
156        stats.add_function_name(&vec![f(2), f(1)]);
157        stats.add_function_name(&vec![f(3), f(1)]);
158        stats.add_function_name(&vec![f(2), f(3), f(1)]);
159
160        let expected = "% self  % total  name
161 40.00    60.00  func3 - file3.rb:3
162 40.00    60.00  func2 - file2.rb:2
163 20.00   100.00  func1 - file1.rb:1
164";
165
166        let mut buf: Vec<u8> = Vec::new();
167        stats.write(&mut buf).expect("summary write failed");
168        let actual = String::from_utf8(buf).expect("summary output not utf8");
169        assert_eq!(actual, expected, "Unexpected summary output");
170    }
171
172    #[test]
173    fn stats_by_line_number() {
174        let mut stats = Stats::new();
175
176        stats.add_lineno(&vec![f(1)]);
177        stats.add_lineno(&vec![f(3), f(2), f(1)]);
178        stats.add_lineno(&vec![f(2), f(1)]);
179        stats.add_lineno(&vec![f(3), f(1)]);
180        stats.add_lineno(&vec![f(2), f(3), f(1)]);
181
182        let expected = "% self  % total  name
183 40.00    60.00  func3 - file3.rb:3
184 40.00    60.00  func2 - file2.rb:2
185 20.00   100.00  func1 - file1.rb:1
186";
187
188        let mut buf: Vec<u8> = Vec::new();
189        stats.write(&mut buf).expect("summary write failed");
190        let actual = String::from_utf8(buf).expect("summary output not utf8");
191        assert_eq!(actual, expected, "Unexpected summary output");
192    }
193}