pyroscope_rbspy_oncpu/ui/
summary.rs1use 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 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 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 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}