pyroscope_rbspy_oncpu/ui/
pprof.rs

1use flate2::{write::GzEncoder, Compression};
2use std::collections::HashMap;
3use std::io::prelude::*;
4use std::time::SystemTime;
5
6use crate::core::types::{StackFrame, StackTrace};
7
8use anyhow::Result;
9
10use prost::Message; // for encode and decode methods below
11
12pub mod pprofs {
13    include!("perftools.profiles.rs");
14}
15use self::pprofs::{Function, Label, Line, Location, Profile, Sample, ValueType};
16
17#[derive(Default)]
18pub struct Stats {
19    profile: Profile,
20    known_frames: HashMap<StackFrame, u64>,
21    prev_time: Option<SystemTime>,
22}
23
24impl Stats {
25    pub fn new() -> Stats {
26        Stats {
27            profile: Profile {
28                // string index 0 must point to "" according to the .proto spec, while "wall" and "nanoseconds" are for our sample_type field
29                string_table: vec![
30                    "".to_string(),
31                    "wall".to_string(),
32                    "nanoseconds".to_string(),
33                ],
34                sample_type: vec![ValueType { r#type: 1, unit: 2 }], // 1 and 2 are indexes from string_table
35                ..Profile::default()
36            },
37            ..Stats::default()
38        }
39    }
40
41    pub fn record(&mut self, stack: &StackTrace) -> Result<()> {
42        let this_time = stack.time.unwrap_or_else(SystemTime::now);
43        let ns_since_last_sample = match self.prev_time {
44            Some(prev_time) => match this_time.duration_since(prev_time) {
45                Ok(duration) => duration.as_nanos(),
46                Err(e) => {
47                    // It's possible that samples will arrive out of order, e.g. if we're sampling
48                    // from multiple processes.
49                    warn!("sample arrived out of order: {}", e);
50                    0
51                }
52            },
53            None => 0,
54        } as i64;
55        self.add_sample(stack, ns_since_last_sample);
56        self.prev_time = Some(this_time);
57        Ok(())
58    }
59
60    fn add_sample(&mut self, stack: &StackTrace, sample_time: i64) {
61        let s = Sample {
62            location_id: self.location_ids(stack),
63            value: vec![sample_time],
64            label: self.labels(stack),
65        };
66        self.profile.sample.push(s);
67    }
68
69    fn location_ids(&mut self, stack: &StackTrace) -> Vec<u64> {
70        let mut ids = <Vec<u64>>::new();
71
72        for frame in &stack.trace {
73            ids.push(self.get_or_create_location_id(frame));
74        }
75        ids
76    }
77
78    fn get_or_create_location_id(&mut self, frame: &StackFrame) -> u64 {
79        // our lookup table has the arbitrary ids (1..n) we use for location ids
80        if let Some(id) = self.known_frames.get(frame) {
81            *id
82        } else {
83            let next_id = self.known_frames.len() as u64 + 1; //ids must be non-0, so start at 1
84            self.known_frames.insert(frame.clone(), next_id); // add to our lookup table
85            let newloc = self.new_location(next_id, frame); // use the same id for the location table
86            self.profile.location.push(newloc);
87            next_id
88        }
89    }
90
91    fn new_location(&mut self, id: u64, frame: &StackFrame) -> Location {
92        let new_line = Line {
93            function_id: self.get_or_create_function_id(frame),
94            line: frame.lineno.unwrap_or(0) as i64,
95        };
96        Location {
97            id,
98            line: vec![new_line],
99            ..Location::default()
100        }
101    }
102
103    fn get_or_create_function_id(&mut self, frame: &StackFrame) -> u64 {
104        let strings = &self.profile.string_table;
105        let mut functions = self.profile.function.iter();
106        if let Some(function) = functions.find(|f| {
107            frame.name == strings[f.name as usize]
108                && frame.relative_path == strings[f.filename as usize]
109        }) {
110            function.id
111        } else {
112            let functions = self.profile.function.iter();
113            let mapped_iter = functions.map(|f| f.id);
114            let max_map = mapped_iter.max();
115            let next_id = match max_map {
116                Some(id) => id + 1,
117                None => 1,
118            };
119            let f = self.new_function(next_id, frame);
120            self.profile.function.push(f);
121            next_id
122        }
123    }
124
125    fn new_function(&mut self, id: u64, frame: &StackFrame) -> Function {
126        Function {
127            id,
128            name: self.string_id(&frame.name),
129            filename: self.string_id(&frame.relative_path),
130            ..Function::default()
131        }
132    }
133
134    fn string_id(&mut self, text: &str) -> i64 {
135        let strings = &mut self.profile.string_table;
136        if let Some(id) = strings.iter().position(|s| *s == *text) {
137            id as i64
138        } else {
139            let next_id = strings.len() as i64;
140            strings.push((*text).to_owned());
141            next_id
142        }
143    }
144
145    fn labels(&mut self, stack: &StackTrace) -> Vec<Label> {
146        let mut labels: Vec<Label> = Vec::new();
147        if let Some(pid) = stack.pid {
148            labels.push(Label {
149                key: self.string_id(&"pid".to_string()),
150                num: pid as i64,
151                ..Label::default()
152            });
153        }
154        if let Some(thread_id) = stack.thread_id {
155            labels.push(Label {
156                key: self.string_id(&"thread_id".to_string()),
157                num: thread_id as i64,
158                ..Label::default()
159            });
160        }
161        labels
162    }
163
164    pub fn write(&mut self, w: &mut dyn Write) -> Result<()> {
165        let mut pprof_data = Vec::new();
166        let mut gzip = GzEncoder::new(Vec::new(), Compression::default());
167
168        self.profile.encode(&mut pprof_data)?;
169        gzip.write_all(&pprof_data)?;
170        w.write_all(&gzip.finish()?)?;
171
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use crate::ui::pprof::*;
179    use flate2::read::GzDecoder;
180    use std::time::Duration;
181
182    // Build a test stacktrace
183    fn s(frames: Vec<StackFrame>, time: SystemTime) -> StackTrace {
184        StackTrace {
185            trace: frames,
186            pid: Some(9),
187            thread_id: Some(999),
188            time: Some(time),
189        }
190    }
191
192    // Build a test stackframe
193    fn f(i: usize) -> StackFrame {
194        StackFrame {
195            name: format!("func{}", i),
196            relative_path: format!("file{}.rb", i),
197            absolute_path: None,
198            lineno: Some(i),
199        }
200    }
201
202    // A stack frame from the same file as another one
203    fn fdup() -> StackFrame {
204        StackFrame {
205            name: "funcX".to_owned(),
206            relative_path: "file1.rb".to_owned(),
207            absolute_path: None,
208            lineno: Some(42),
209        }
210    }
211
212    fn test_stats() -> Stats {
213        let mut stats = Stats::new();
214        let mut time = SystemTime::now();
215        stats.record(&s(vec![f(1)], time)).unwrap();
216        time += Duration::new(0, 200);
217        stats.record(&s(vec![f(3), f(2), f(1)], time)).unwrap();
218        time += Duration::new(0, 400);
219        stats.record(&s(vec![f(2), f(1)], time)).unwrap();
220        time += Duration::new(0, 600);
221        stats.record(&s(vec![f(3), f(1)], time)).unwrap();
222        time += Duration::new(0, 800);
223        stats.record(&s(vec![f(2), f(1)], time)).unwrap();
224        time += Duration::new(0, 1000);
225        stats.record(&s(vec![f(3), fdup(), f(1)], time)).unwrap();
226
227        stats
228    }
229
230    #[test]
231    fn tolerate_stacktrace_timestamps_arriving_out_of_order() {
232        let mut stats = Stats::new();
233        let mut time = SystemTime::now();
234        stats.record(&s(vec![f(1)], time)).unwrap();
235        time -= Duration::new(0, 200);
236        stats.record(&s(vec![f(3), f(2), f(1)], time)).unwrap();
237    }
238
239    #[test]
240    fn can_collect_traces_and_write_to_pprof_format() {
241        let mut gz_stats_buf: Vec<u8> = Vec::new();
242        let mut stats = test_stats();
243        stats.write(&mut gz_stats_buf).expect("write failed");
244
245        let mut gz = GzDecoder::new(&*gz_stats_buf);
246        let mut stats_buf = Vec::new();
247        gz.read_to_end(&mut stats_buf).unwrap();
248
249        let actual = pprofs::Profile::decode(&*stats_buf).expect("decode failed");
250        let expected = Profile {
251            sample_type: vec![ValueType { r#type: 1, unit: 2 }],
252            sample: vec![
253                Sample {
254                    location_id: vec![1],
255                    value: vec![0],
256                    label: vec![
257                        Label {
258                            key: 5,
259                            str: 0,
260                            num: 9,
261                            num_unit: 0,
262                        },
263                        Label {
264                            key: 6,
265                            str: 0,
266                            num: 999,
267                            num_unit: 0,
268                        },
269                    ],
270                },
271                Sample {
272                    location_id: vec![2, 3, 1],
273                    value: vec![200],
274                    label: vec![
275                        Label {
276                            key: 5,
277                            str: 0,
278                            num: 9,
279                            num_unit: 0,
280                        },
281                        Label {
282                            key: 6,
283                            str: 0,
284                            num: 999,
285                            num_unit: 0,
286                        },
287                    ],
288                },
289                Sample {
290                    location_id: vec![3, 1],
291                    value: vec![400],
292                    label: vec![
293                        Label {
294                            key: 5,
295                            str: 0,
296                            num: 9,
297                            num_unit: 0,
298                        },
299                        Label {
300                            key: 6,
301                            str: 0,
302                            num: 999,
303                            num_unit: 0,
304                        },
305                    ],
306                },
307                Sample {
308                    location_id: vec![2, 1],
309                    value: vec![600],
310                    label: vec![
311                        Label {
312                            key: 5,
313                            str: 0,
314                            num: 9,
315                            num_unit: 0,
316                        },
317                        Label {
318                            key: 6,
319                            str: 0,
320                            num: 999,
321                            num_unit: 0,
322                        },
323                    ],
324                },
325                Sample {
326                    location_id: vec![3, 1],
327                    value: vec![800],
328                    label: vec![
329                        Label {
330                            key: 5,
331                            str: 0,
332                            num: 9,
333                            num_unit: 0,
334                        },
335                        Label {
336                            key: 6,
337                            str: 0,
338                            num: 999,
339                            num_unit: 0,
340                        },
341                    ],
342                },
343                Sample {
344                    location_id: vec![2, 4, 1],
345                    value: vec![1000],
346                    label: vec![
347                        Label {
348                            key: 5,
349                            str: 0,
350                            num: 9,
351                            num_unit: 0,
352                        },
353                        Label {
354                            key: 6,
355                            str: 0,
356                            num: 999,
357                            num_unit: 0,
358                        },
359                    ],
360                },
361            ],
362            mapping: vec![],
363            location: vec![
364                Location {
365                    id: 1,
366                    mapping_id: 0,
367                    address: 0,
368                    line: vec![Line {
369                        function_id: 1,
370                        line: 1,
371                    }],
372                    is_folded: false,
373                },
374                Location {
375                    id: 2,
376                    mapping_id: 0,
377                    address: 0,
378                    line: vec![Line {
379                        function_id: 2,
380                        line: 3,
381                    }],
382                    is_folded: false,
383                },
384                Location {
385                    id: 3,
386                    mapping_id: 0,
387                    address: 0,
388                    line: vec![Line {
389                        function_id: 3,
390                        line: 2,
391                    }],
392                    is_folded: false,
393                },
394                Location {
395                    id: 4,
396                    mapping_id: 0,
397                    address: 0,
398                    line: vec![Line {
399                        function_id: 4,
400                        line: 42,
401                    }],
402                    is_folded: false,
403                },
404            ],
405            function: vec![
406                Function {
407                    id: 1,
408                    name: 3,
409                    system_name: 0,
410                    filename: 4,
411                    start_line: 0,
412                },
413                Function {
414                    id: 2,
415                    name: 7,
416                    system_name: 0,
417                    filename: 8,
418                    start_line: 0,
419                },
420                Function {
421                    id: 3,
422                    name: 9,
423                    system_name: 0,
424                    filename: 10,
425                    start_line: 0,
426                },
427                Function {
428                    id: 4,
429                    name: 11,
430                    system_name: 0,
431                    filename: 4,
432                    start_line: 0,
433                },
434            ],
435            string_table: vec![
436                "".to_string(),
437                "wall".to_string(),
438                "nanoseconds".to_string(),
439                "func1".to_string(),
440                "file1.rb".to_string(),
441                "pid".to_string(),
442                "thread_id".to_string(),
443                "func3".to_string(),
444                "file3.rb".to_string(),
445                "func2".to_string(),
446                "file2.rb".to_string(),
447                "funcX".to_string(),
448            ],
449            drop_frames: 0,
450            keep_frames: 0,
451            time_nanos: 0,
452            duration_nanos: 0,
453            period_type: None,
454            period: 0,
455            comment: vec![],
456            default_sample_type: 0,
457        };
458        assert_eq!(actual, expected, "stats don't match");
459    }
460}