pprof/
report.rs

1// Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
2
3use std::collections::HashMap;
4use std::fmt::{Debug, Formatter};
5
6use parking_lot::RwLock;
7
8use crate::frames::{Frames, UnresolvedFrames};
9use crate::profiler::Profiler;
10use crate::timer::ReportTiming;
11
12use crate::{Error, Result};
13
14/// The final presentation of a report which is actually an `HashMap` from `Frames` to isize (count).
15pub struct Report {
16    /// Key is a backtrace captured by profiler and value is count of it.
17    pub data: HashMap<Frames, isize>,
18
19    /// Collection frequency, start time, duration.
20    pub timing: ReportTiming,
21}
22
23/// The presentation of an unsymbolicated report which is actually an `HashMap` from `UnresolvedFrames` to isize (count).
24pub struct UnresolvedReport {
25    /// key is a backtrace captured by profiler and value is count of it.
26    pub data: HashMap<UnresolvedFrames, isize>,
27
28    /// Collection frequency, start time, duration.
29    pub timing: ReportTiming,
30}
31
32type FramesPostProcessor = Box<dyn Fn(&mut Frames)>;
33
34/// A builder of `Report` and `UnresolvedReport`. It builds report from a running `Profiler`.
35pub struct ReportBuilder<'a> {
36    frames_post_processor: Option<FramesPostProcessor>,
37    profiler: &'a RwLock<Result<Profiler>>,
38    timing: ReportTiming,
39}
40
41impl<'a> ReportBuilder<'a> {
42    pub(crate) fn new(profiler: &'a RwLock<Result<Profiler>>, timing: ReportTiming) -> Self {
43        Self {
44            frames_post_processor: None,
45            profiler,
46            timing,
47        }
48    }
49
50    /// Set `frames_post_processor` of a `ReportBuilder`. Before finally building a report, `frames_post_processor`
51    /// will be applied to every Frames.
52    pub fn frames_post_processor<T>(&mut self, frames_post_processor: T) -> &mut Self
53    where
54        T: Fn(&mut Frames) + 'static,
55    {
56        self.frames_post_processor
57            .replace(Box::new(frames_post_processor));
58
59        self
60    }
61
62    /// Build an `UnresolvedReport`
63    pub fn build_unresolved(&self) -> Result<UnresolvedReport> {
64        let mut hash_map = HashMap::new();
65
66        match self.profiler.read().as_ref() {
67            Err(err) => {
68                log::error!("Error in creating profiler: {}", err);
69                Err(Error::CreatingError)
70            }
71            Ok(profiler) => {
72                profiler.data.try_iter()?.for_each(|entry| {
73                    let count = entry.count;
74                    if count > 0 {
75                        let key = &entry.item;
76                        match hash_map.get_mut(key) {
77                            Some(value) => {
78                                *value += count;
79                            }
80                            None => {
81                                match hash_map.insert(key.clone(), count) {
82                                    None => {}
83                                    Some(_) => {
84                                        unreachable!();
85                                    }
86                                };
87                            }
88                        }
89                    }
90                });
91
92                Ok(UnresolvedReport {
93                    data: hash_map,
94                    timing: self.timing.clone(),
95                })
96            }
97        }
98    }
99
100    /// Build a `Report`.
101    pub fn build(&self) -> Result<Report> {
102        let mut hash_map = HashMap::new();
103
104        match self.profiler.write().as_mut() {
105            Err(err) => {
106                log::error!("Error in creating profiler: {}", err);
107                Err(Error::CreatingError)
108            }
109            Ok(profiler) => {
110                profiler.data.try_iter()?.for_each(|entry| {
111                    let count = entry.count;
112                    if count > 0 {
113                        let mut key = Frames::from(entry.item.clone());
114                        if let Some(processor) = &self.frames_post_processor {
115                            processor(&mut key);
116                        }
117
118                        match hash_map.get_mut(&key) {
119                            Some(value) => {
120                                *value += count;
121                            }
122                            None => {
123                                match hash_map.insert(key, count) {
124                                    None => {}
125                                    Some(_) => {
126                                        unreachable!();
127                                    }
128                                };
129                            }
130                        }
131                    }
132                });
133
134                Ok(Report {
135                    data: hash_map,
136                    timing: self.timing.clone(),
137                })
138            }
139        }
140    }
141}
142
143/// This will generate Report in a human-readable format:
144///
145/// ```shell
146/// FRAME: pprof::profiler::perf_signal_handler::h7b995c4ab2e66493 -> FRAME: Unknown -> FRAME: {func1} ->
147/// FRAME: {func2} -> FRAME: {func3} ->  THREAD: {thread_name} {count}
148/// ```
149impl Debug for Report {
150    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
151        for (key, val) in self.data.iter() {
152            write!(f, "{:?} {}", key, val)?;
153            writeln!(f)?;
154        }
155
156        Ok(())
157    }
158}
159
160#[cfg(feature = "flamegraph")]
161mod flamegraph {
162    use super::*;
163    use inferno::flamegraph;
164    use std::fmt::Write;
165
166    impl Report {
167        /// `flamegraph` will write an svg flamegraph into `writer` **only available with `flamegraph` feature**
168        pub fn flamegraph<W>(&self, writer: W) -> Result<()>
169        where
170            W: std::io::Write,
171        {
172            self.flamegraph_with_options(writer, &mut flamegraph::Options::default())
173        }
174
175        /// same as `flamegraph`, but accepts custom `options` for the flamegraph
176        pub fn flamegraph_with_options<W>(
177            &self,
178            writer: W,
179            options: &mut flamegraph::Options,
180        ) -> Result<()>
181        where
182            W: std::io::Write,
183        {
184            let lines: Vec<String> = self
185                .data
186                .iter()
187                .map(|(key, value)| {
188                    let mut line = key.thread_name_or_id();
189                    line.push(';');
190
191                    for frame in key.frames.iter().rev() {
192                        for symbol in frame.iter().rev() {
193                            write!(&mut line, "{};", symbol).unwrap();
194                        }
195                    }
196
197                    line.pop().unwrap_or_default();
198                    write!(&mut line, " {}", value).unwrap();
199
200                    line
201                })
202                .collect();
203            if !lines.is_empty() {
204                flamegraph::from_lines(options, lines.iter().map(|s| &**s), writer).unwrap();
205                // TODO: handle this error
206            }
207
208            Ok(())
209        }
210    }
211}
212
213#[cfg(feature = "_protobuf")]
214#[allow(clippy::useless_conversion)]
215#[allow(clippy::needless_update)]
216mod protobuf {
217    use super::*;
218    use crate::protos;
219    use std::collections::HashSet;
220    use std::time::SystemTime;
221
222    const SAMPLES: &str = "samples";
223    const COUNT: &str = "count";
224    const CPU: &str = "cpu";
225    const NANOSECONDS: &str = "nanoseconds";
226    const THREAD: &str = "thread";
227
228    impl Report {
229        /// `pprof` will generate google's pprof format report.
230        pub fn pprof(&self) -> crate::Result<protos::Profile> {
231            let mut dedup_str = HashSet::new();
232            for key in self.data.keys() {
233                dedup_str.insert(key.thread_name_or_id());
234                for frame in key.frames.iter() {
235                    for symbol in frame {
236                        dedup_str.insert(symbol.name());
237                        dedup_str.insert(symbol.sys_name().into_owned());
238                        dedup_str.insert(symbol.filename().into_owned());
239                    }
240                }
241            }
242            dedup_str.insert(SAMPLES.into());
243            dedup_str.insert(COUNT.into());
244            dedup_str.insert(CPU.into());
245            dedup_str.insert(NANOSECONDS.into());
246            dedup_str.insert(THREAD.into());
247            // string table's first element must be an empty string
248            let mut str_tbl = vec!["".to_owned()];
249            str_tbl.extend(dedup_str.into_iter());
250
251            let mut strings = HashMap::new();
252            for (index, name) in str_tbl.iter().enumerate() {
253                strings.insert(name.as_str(), index);
254            }
255
256            let mut samples = vec![];
257            let mut loc_tbl = vec![];
258            let mut fn_tbl = vec![];
259            let mut functions = HashMap::new();
260            for (key, count) in self.data.iter() {
261                let mut locs = vec![];
262                for frame in key.frames.iter() {
263                    for symbol in frame {
264                        let name = symbol.name();
265                        if let Some(loc_idx) = functions.get(&name) {
266                            locs.push(*loc_idx);
267                            continue;
268                        }
269                        let sys_name = symbol.sys_name();
270                        let filename = symbol.filename();
271                        let lineno = symbol.lineno();
272                        let function_id = fn_tbl.len() as u64 + 1;
273                        let function = protos::Function {
274                            id: function_id,
275                            name: *strings.get(name.as_str()).unwrap() as i64,
276                            system_name: *strings.get(sys_name.as_ref()).unwrap() as i64,
277                            filename: *strings.get(filename.as_ref()).unwrap() as i64,
278                            ..protos::Function::default()
279                        };
280                        functions.insert(name, function_id);
281                        let line = protos::Line {
282                            function_id,
283                            line: lineno as i64,
284                            ..protos::Line::default()
285                        };
286                        let loc = protos::Location {
287                            id: function_id,
288                            line: vec![line].into(),
289                            ..protos::Location::default()
290                        };
291                        // the fn_tbl has the same length with loc_tbl
292                        fn_tbl.push(function);
293                        loc_tbl.push(loc);
294                        // current frame locations
295                        locs.push(function_id);
296                    }
297                }
298                let thread_name = protos::Label {
299                    key: *strings.get(THREAD).unwrap() as i64,
300                    str: *strings.get(&key.thread_name_or_id().as_str()).unwrap() as i64,
301                    ..protos::Label::default()
302                };
303                let sample = protos::Sample {
304                    location_id: locs,
305                    value: vec![
306                        *count as i64,
307                        *count as i64 * 1_000_000_000 / self.timing.frequency as i64,
308                    ],
309                    label: vec![thread_name].into(),
310                    ..Default::default()
311                };
312                samples.push(sample);
313            }
314            let samples_value = protos::ValueType {
315                ty: *strings.get(SAMPLES).unwrap() as i64,
316                unit: *strings.get(COUNT).unwrap() as i64,
317                ..Default::default()
318            };
319            let time_value = protos::ValueType {
320                ty: *strings.get(CPU).unwrap() as i64,
321                unit: *strings.get(NANOSECONDS).unwrap() as i64,
322                ..Default::default()
323            };
324            let profile = protos::Profile {
325                sample_type: vec![samples_value, time_value.clone()].into(),
326                sample: samples.into(),
327                string_table: str_tbl.into(),
328                function: fn_tbl.into(),
329                location: loc_tbl.into(),
330                time_nanos: self
331                    .timing
332                    .start_time
333                    .duration_since(SystemTime::UNIX_EPOCH)
334                    .unwrap_or_default()
335                    .as_nanos() as i64,
336                duration_nanos: self.timing.duration.as_nanos() as i64,
337                period_type: Some(time_value).into(),
338                period: 1_000_000_000 / self.timing.frequency as i64,
339                ..protos::Profile::default()
340            };
341            Ok(profile)
342        }
343    }
344}