pyroscope_rbspy_oncpu/ui/
flamegraph.rs1use anyhow::Result;
2use inferno::flamegraph::{Direction, Options};
3use std::collections::HashMap;
4use std::io::Write;
5
6use crate::core::types::StackFrame;
7
8#[derive(Default)]
10pub struct Stats {
11 pub counts: HashMap<String, usize>,
12}
13
14impl Stats {
15 pub fn record(&mut self, stack: &[StackFrame]) -> Result<()> {
16 let frame = stack
17 .iter()
18 .rev()
19 .map(|frame| format!("{}", frame))
20 .collect::<Vec<String>>()
21 .join(";");
22
23 *self.counts.entry(frame).or_insert(0) += 1;
24 Ok(())
25 }
26
27 pub fn write_flamegraph<W: Write>(&self, w: W, min_width: f64) -> Result<()> {
28 if self.is_empty() {
29 eprintln!("Warning: no profile samples were collected");
30 } else {
31 let mut opts = Options::default();
32 opts.direction = Direction::Inverted;
33 opts.hash = true;
34 opts.min_width = min_width;
35 inferno::flamegraph::from_lines(
36 &mut opts,
37 self.get_lines().iter().map(|x| x.as_str()),
38 w,
39 )?;
40 }
41
42 Ok(())
43 }
44
45 pub fn write_collapsed<W: Write>(&self, w: &mut W) -> Result<()> {
46 if self.is_empty() {
47 eprintln!("Warning: no profile samples were collected");
48 } else {
49 self.get_lines()
50 .iter()
51 .try_for_each(|line| write!(w, "{}\n", line))?;
52 }
53 Ok(())
54 }
55
56 fn get_lines(&self) -> Vec<String> {
57 self.counts
58 .iter()
59 .map(|(frame, count)| format!("{} {}", frame, count))
60 .collect()
61 }
62
63 fn is_empty(&self) -> bool {
64 self.counts.is_empty()
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use crate::ui::flamegraph::*;
71 use std::io::Cursor;
72
73 fn f(i: usize) -> StackFrame {
75 StackFrame {
76 name: format!("func{}", i),
77 relative_path: format!("file{}.rb", i),
78 absolute_path: None,
79 lineno: Some(i),
80 }
81 }
82
83 fn build_stats() -> Result<Stats> {
85 let mut stats = Stats::default();
86 stats.record(&vec![f(1)])?;
87 stats.record(&vec![f(2), f(1)])?;
88 stats.record(&vec![f(2), f(1)])?;
89 stats.record(&vec![f(2), f(3), f(1)])?;
90 stats.record(&vec![f(2), f(3), f(1)])?;
91 stats.record(&vec![f(2), f(3), f(1)])?;
92 Ok(stats)
93 }
94
95 fn assert_contains(counts: &HashMap<String, usize>, s: &str, val: usize) {
96 assert_eq!(counts.get(&s.to_string()), Some(&val));
97 }
98
99 #[test]
100 fn test_stats() -> Result<()> {
101 let stats = build_stats()?;
102 let counts = &stats.counts;
103 assert_contains(counts, "func1 - file1.rb:1", 1);
104 assert_contains(
105 counts,
106 "func1 - file1.rb:1;func3 - file3.rb:3;func2 - file2.rb:2",
107 3,
108 );
109 assert_contains(counts, "func1 - file1.rb:1;func2 - file2.rb:2", 2);
110
111 Ok(())
112 }
113
114 #[test]
115 fn test_collapsed() -> Result<()> {
116 let stats = build_stats()?;
117 let mut writer = Cursor::new(Vec::<u8>::new());
118 stats.write_collapsed(&mut writer)?;
119 let collapsed_text = std::str::from_utf8(writer.get_ref())?;
120 assert!(collapsed_text.contains("func1 - file1.rb:1 1"));
121 assert!(
122 collapsed_text.contains("func1 - file1.rb:1;func3 - file3.rb:3;func2 - file2.rb:2 3")
123 );
124 assert!(collapsed_text.contains("func1 - file1.rb:1;func2 - file2.rb:2 2"));
125
126 Ok(())
127 }
128
129 #[test]
130 fn test_flamegraph_from_collapsed() -> Result<()> {
131 let stats = build_stats()?;
132
133 let mut writer = Cursor::new(Vec::<u8>::new());
134 stats.write_collapsed(&mut writer)?;
135
136 let collapsed_reader = Cursor::new(writer.into_inner());
137 let svg_writer = Cursor::new(Vec::new());
138
139 let mut opts = Options::default();
140 opts.direction = Direction::Inverted;
141 opts.min_width = 0.1;
142 inferno::flamegraph::from_reader(&mut opts, collapsed_reader, svg_writer)?;
143
144 Ok(())
145 }
146}