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; pub 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_table: vec![
30 "".to_string(),
31 "wall".to_string(),
32 "nanoseconds".to_string(),
33 ],
34 sample_type: vec![ValueType { r#type: 1, unit: 2 }], ..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 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 if let Some(id) = self.known_frames.get(frame) {
81 *id
82 } else {
83 let next_id = self.known_frames.len() as u64 + 1; self.known_frames.insert(frame.clone(), next_id); let newloc = self.new_location(next_id, frame); 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 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 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 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}