1use std::{borrow::Cow, collections::VecDeque, ffi::c_void, fmt::Debug, io::BufRead, path::Path};
2
3use serde::Serialize;
4
5#[derive(Debug, Clone)]
6pub struct FrameInfo {
7 pub filename: Option<std::path::PathBuf>,
9 pub colno: Option<u32>,
11 pub lineno: Option<u32>,
13 pub fn_address: Option<*mut c_void>,
15 pub fn_name: Option<String>,
17}
18
19#[derive(Debug)]
20pub struct Allocation {
21 pub allocation_size: usize,
23 pub deallocation_size: usize,
25 pub address: usize,
27 pub stack: VecDeque<FrameInfo>,
29}
30
31#[derive(Debug)]
32pub struct Stats {
33 pub allocations: VecDeque<Allocation>,
35 pub deallocations: VecDeque<Allocation>,
37}
38
39impl Stats {
40 pub fn into_tree(mut self) -> Result<Tree<Key>, Cow<'static, str>> {
42 let cwd = std::env::current_dir()
43 .map_err(|e| format!("failed to get current directory: {:?}", e))?;
44 let cwd = cwd.to_str().ok_or("current directory is not valid UTF-8")?;
45
46 let allocation = match self.allocations.pop_back() {
47 None => {
48 return Err("no allocations".into());
49 }
50 Some(allocation) => allocation,
51 };
52
53 let mut stack = allocation.stack;
54
55 let initial_key = loop {
56 let root = stack.pop_front();
57 let root = match root {
58 None => panic!("stack is empty"),
59 Some(root) => root,
60 };
61 let key = TryInto::<Key>::try_into(root);
62 if let Ok(key) = key {
63 break key;
64 }
65 };
66
67 let mut tree = Tree {
68 category: guess_category(cwd, initial_key.filename.as_str()),
69 key: initial_key.clone(),
70 allocation: 0,
71 allocation_count: 0,
72 deallocation: 0,
73 deallocation_count: 0,
74 children: Vec::new(),
75 };
76 let mut pointer = &mut tree;
77 let stack_len = stack.len();
78 for (index, info) in stack.into_iter().enumerate() {
79 let is_last = stack_len == index + 1;
80 let key: Key = match info.try_into() {
81 Ok(key) => key,
82 Err(_) => continue,
83 };
84
85 let mut c = Tree {
86 category: guess_category(cwd, key.filename.as_str()),
87 key,
88 allocation: 0,
89 allocation_count: 0,
90 deallocation: 0,
91 deallocation_count: 0,
92 children: Vec::new(),
93 };
94 if is_last {
95 c.allocation += allocation.allocation_size;
96 c.allocation_count += 1;
97 c.deallocation += allocation.deallocation_size;
98 }
99 pointer.children.push(c);
100 pointer = pointer.children.last_mut().unwrap();
101 }
102
103 for mut allocation in self.allocations {
104 let mut pointer = &mut tree;
105
106 let first_key: Key = loop {
112 let s = match allocation.stack.pop_front() {
113 None => panic!("stack is empty"),
114 Some(s) => s,
115 };
116 match s.try_into() {
117 Ok(k) => break k,
118 Err(_) => continue,
119 };
120 };
121 if first_key != initial_key {
122 panic!("first frame of allocation is not the same as the first frame of the tree");
123 }
124
125 let stack_len = allocation.stack.len();
126 for (index, info) in allocation.stack.into_iter().enumerate() {
127 let is_last = stack_len == index + 1;
128 let key: Key = match info.try_into() {
129 Ok(key) => key,
130 Err(_) => continue,
131 };
132
133 let found = pointer.children.iter().position(|c| c.key == key);
134 pointer = if let Some(found) = found {
135 pointer.children.get_mut(found).unwrap()
136 } else {
137 let c = Tree {
138 category: guess_category(cwd, key.filename.as_str()),
139 key,
140 allocation: 0,
141 allocation_count: 0,
142 deallocation: 0,
143 deallocation_count: 0,
144 children: Vec::new(),
145 };
146 pointer.children.push(c);
147 pointer.children.last_mut().unwrap()
148 };
149
150 if is_last {
152 pointer.allocation += allocation.allocation_size;
153 pointer.deallocation += allocation.deallocation_size;
154 pointer.allocation_count += 1;
155 }
156 }
157 }
158
159 for mut allocation in self.deallocations {
160 let mut pointer = &mut tree;
161
162 let first_key: Key = loop {
168 let s = match allocation.stack.pop_front() {
169 None => panic!("stack is empty"),
170 Some(s) => s,
171 };
172 match s.try_into() {
173 Ok(k) => break k,
174 Err(_) => continue,
175 };
176 };
177 if first_key != initial_key {
178 panic!("first frame of allocation is not the same as the first frame of the tree");
179 }
180
181 let stack_len = allocation.stack.len();
182 for (index, info) in allocation.stack.into_iter().enumerate() {
183 let is_last = stack_len == index + 1;
184 let key: Key = match info.try_into() {
185 Ok(key) => key,
186 Err(_) => continue,
187 };
188
189 let found = pointer.children.iter().position(|c| c.key == key);
190 pointer = if let Some(found) = found {
191 pointer.children.get_mut(found).unwrap()
192 } else {
193 let c = Tree {
194 category: guess_category(cwd, key.filename.as_str()),
195 key,
196 allocation: 0,
197 allocation_count: 0,
198 deallocation: 0,
199 deallocation_count: 0,
200 children: Vec::new(),
201 };
202 pointer.children.push(c);
203 pointer.children.last_mut().unwrap()
204 };
205
206 if is_last {
208 pointer.allocation += allocation.allocation_size;
209 pointer.deallocation += allocation.deallocation_size;
210 pointer.deallocation_count += 1;
211 }
212 }
213 }
214
215 tree.update_value();
216
217 Ok(tree)
218 }
219}
220
221#[derive(Debug, PartialEq, Eq, Serialize, Clone)]
222pub struct FileContent {
223 pub before: Vec<String>,
224 pub highlighted: String,
225 pub after: Vec<String>,
226}
227
228#[derive(Debug, PartialEq, Eq, Serialize, Clone)]
229pub struct Key {
230 pub filename: String,
231 pub colno: u32,
232 pub lineno: u32,
233 #[serde(skip_serializing)]
234 pub fn_address: *mut c_void,
235 pub fn_name: String,
236 pub file_content: Option<FileContent>,
237}
238
239impl TryFrom<FrameInfo> for Key {
240 type Error = &'static str;
241
242 fn try_from(value: FrameInfo) -> Result<Self, Self::Error> {
243 let filename = value.filename.ok_or("filename is None")?;
244 let filename = filename.to_str().ok_or("filename is not valid UTF-8")?;
245 let filename = filename.to_string();
246
247 let colno = value.colno.ok_or("colno is None")?;
248 let lineno = value.lineno.ok_or("lineno is None")?;
249 let fn_address = value.fn_address.ok_or("fn_address is None")?;
250 let fn_name = value.fn_name.ok_or("fn_name is None")?;
251
252 let fn_name = rustc_demangle::demangle(&fn_name).to_string();
253
254 let delta = 5;
255 let range_min = if lineno > (delta + 1) {
256 lineno - delta - 1
257 } else {
258 0
259 };
260 let file_content = std::fs::File::open(&filename).ok().and_then(|file| {
261 let lines = std::io::BufReader::new(file).lines();
262 let mut lines: Vec<_> = lines
263 .enumerate()
264 .filter_map(|(i, line)| Some((i, line.ok()?)))
265 .skip_while(|(index, _)| *index < range_min as usize)
266 .take_while(|(index, _)| *index < lineno as usize + delta as usize)
267 .collect();
268
269 let highlighted_index = match lines.iter().position(|(i, _)| *i as u32 == lineno) {
270 Some(i) => i,
271 None => {
272 println!("not found");
273 return None;
274 }
275 };
276
277 let mut after = lines.split_off(highlighted_index - 1);
278 let highlighted = after.remove(0);
279 let before = lines;
280
281 Some(FileContent {
282 before: before.into_iter().map(|(_, line)| line).collect(),
283 highlighted: highlighted.1,
284 after: after.into_iter().map(|(_, line)| line).collect(),
285 })
286 });
287
288 Ok(Key {
289 filename,
290 colno,
291 lineno,
292 fn_address,
293 fn_name,
294 file_content,
295 })
296 }
297}
298
299#[derive(Debug, Serialize)]
300#[cfg_attr(test, derive(PartialEq, Eq))]
301pub struct Tree<K: Debug + Serialize> {
303 pub key: K,
304 pub allocation: usize,
305 pub allocation_count: usize,
306 pub deallocation: usize,
307 pub deallocation_count: usize,
308 pub category: Category,
309 pub children: Vec<Tree<K>>,
310}
311
312impl<K: Debug + Serialize> Tree<K> {
313 pub fn print_flamegraph<P>(&self, path: P)
315 where
316 P: AsRef<Path>,
317 {
318 let d = serde_json::to_string(&self).unwrap();
319 let html = include_str!("../template.html");
320 let html = html.replace("{ undefined }", &d);
321 std::fs::write(path, html).unwrap();
322 }
323
324 fn update_value(&mut self) {
325 let mut allocation = 0;
326 let mut allocation_count = 0;
327 let mut deallocation = 0;
328 let mut deallocation_count = 0;
329 for child in &mut self.children {
330 child.update_value();
331 allocation += child.allocation;
332 if child.allocation > 0 {
333 allocation_count += child.allocation_count;
334 }
335 deallocation += child.deallocation;
336 if child.deallocation > 0 {
337 deallocation_count += child.deallocation_count;
338 }
339 }
340 self.allocation += allocation;
341 self.allocation_count += allocation_count;
342 self.deallocation += deallocation;
343 self.deallocation_count += deallocation_count;
344 }
345}
346
347#[derive(Debug, Serialize)]
348#[cfg_attr(test, derive(PartialEq, Eq))]
349#[serde(rename_all = "lowercase")]
350pub enum Category {
359 RustStdLib,
360 RustC,
361 Deps,
362 Application,
363 Unknown,
364}
365
366fn guess_category(cwd: &str, filename: &str) -> Category {
367 if filename.contains("/rustc/") {
368 Category::RustC
369 } else if filename.contains("/rustlib/") {
370 Category::RustStdLib
371 } else if filename.contains("cargo/registry/src") {
372 Category::Deps
373 } else if filename.starts_with(cwd) {
374 Category::Application
375 } else {
376 Category::Unknown
377 }
378}
379
380#[cfg(test)]
381mod test {
382 use pretty_assertions::assert_eq;
383
384 use super::*;
385
386 #[test]
387 fn test_tree_value_1() {
388 let stats = Stats {
389 deallocations: VecDeque::new(),
390 allocations: VecDeque::from([Allocation {
391 allocation_size: 1024,
392 deallocation_size: 0,
393 address: 0,
394 stack: VecDeque::from([
395 FrameInfo {
396 filename: Some("foo.rs".into()),
397 colno: Some(1),
398 lineno: Some(1),
399 fn_address: Some(std::ptr::null_mut()),
400 fn_name: Some("foo".into()),
401 },
402 FrameInfo {
403 filename: Some("foo2.rs".into()),
404 colno: Some(1),
405 lineno: Some(1),
406 fn_address: Some(std::ptr::null_mut()),
407 fn_name: Some("foo2".into()),
408 },
409 FrameInfo {
410 filename: Some("foo3.rs".into()),
411 colno: Some(1),
412 lineno: Some(1),
413 fn_address: Some(std::ptr::null_mut()),
414 fn_name: Some("foo3".into()),
415 },
416 ]),
417 }]),
418 };
419 let tree = stats.into_tree().unwrap();
420
421 assert_eq!(
422 tree,
423 Tree {
424 key: Key {
425 filename: "foo.rs".to_string(),
426 colno: 1,
427 lineno: 1,
428 fn_address: std::ptr::null_mut(),
429 fn_name: "foo".to_string(),
430 file_content: None,
431 },
432 allocation: 1024,
433 allocation_count: 1,
434 deallocation: 0,
435 deallocation_count: 0,
436 category: Category::Unknown,
437 children: vec![Tree {
438 key: Key {
439 filename: "foo2.rs".to_string(),
440 colno: 1,
441 lineno: 1,
442 fn_address: std::ptr::null_mut(),
443 fn_name: "foo2".to_string(),
444 file_content: None,
445 },
446 allocation: 1024,
447 allocation_count: 1,
448 deallocation: 0,
449 deallocation_count: 0,
450 category: Category::Unknown,
451 children: vec![Tree {
452 key: Key {
453 filename: "foo3.rs".to_string(),
454 colno: 1,
455 lineno: 1,
456 fn_address: std::ptr::null_mut(),
457 fn_name: "foo3".to_string(),
458 file_content: None,
459 },
460 allocation: 1024,
461 allocation_count: 1,
462 deallocation: 0,
463 deallocation_count: 0,
464 category: Category::Unknown,
465 children: vec![],
466 }],
467 }],
468 }
469 );
470 }
471
472 #[test]
473 fn test_tree_value_2() {
474 let stats = Stats {
475 deallocations: VecDeque::new(),
476 allocations: VecDeque::from([
477 Allocation {
478 allocation_size: 1024,
479 deallocation_size: 0,
480 address: 0,
481 stack: VecDeque::from([
482 FrameInfo {
483 filename: Some("foo.rs".into()),
484 colno: Some(1),
485 lineno: Some(1),
486 fn_address: Some(std::ptr::null_mut()),
487 fn_name: Some("foo".into()),
488 },
489 FrameInfo {
490 filename: Some("foo2.rs".into()),
491 colno: Some(1),
492 lineno: Some(1),
493 fn_address: Some(std::ptr::null_mut()),
494 fn_name: Some("foo2".into()),
495 },
496 FrameInfo {
497 filename: Some("foo3.rs".into()),
498 colno: Some(1),
499 lineno: Some(1),
500 fn_address: Some(std::ptr::null_mut()),
501 fn_name: Some("foo3".into()),
502 },
503 ]),
504 },
505 Allocation {
506 allocation_size: 1024,
507 deallocation_size: 0,
508 address: 0,
509 stack: VecDeque::from([
510 FrameInfo {
511 filename: Some("foo.rs".into()),
512 colno: Some(1),
513 lineno: Some(1),
514 fn_address: Some(std::ptr::null_mut()),
515 fn_name: Some("foo".into()),
516 },
517 FrameInfo {
518 filename: Some("foo2.rs".into()),
519 colno: Some(1),
520 lineno: Some(1),
521 fn_address: Some(std::ptr::null_mut()),
522 fn_name: Some("foo2".into()),
523 },
524 FrameInfo {
525 filename: Some("foo3.rs".into()),
526 colno: Some(1),
527 lineno: Some(1),
528 fn_address: Some(std::ptr::null_mut()),
529 fn_name: Some("foo3".into()),
530 },
531 ]),
532 },
533 ]),
534 };
535 let tree = stats.into_tree().unwrap();
536
537 assert_eq!(
538 tree,
539 Tree {
540 key: Key {
541 filename: "foo.rs".to_string(),
542 colno: 1,
543 lineno: 1,
544 fn_address: std::ptr::null_mut(),
545 fn_name: "foo".to_string(),
546 file_content: None,
547 },
548 allocation: 1024 * 2,
549 allocation_count: 2,
550 deallocation: 0,
551 deallocation_count: 0,
552 category: Category::Unknown,
553 children: vec![Tree {
554 key: Key {
555 filename: "foo2.rs".to_string(),
556 colno: 1,
557 lineno: 1,
558 fn_address: std::ptr::null_mut(),
559 fn_name: "foo2".to_string(),
560 file_content: None,
561 },
562 allocation: 1024 * 2,
563 allocation_count: 2,
564 deallocation: 0,
565 deallocation_count: 0,
566 category: Category::Unknown,
567 children: vec![Tree {
568 key: Key {
569 filename: "foo3.rs".to_string(),
570 colno: 1,
571 lineno: 1,
572 fn_address: std::ptr::null_mut(),
573 fn_name: "foo3".to_string(),
574 file_content: None,
575 },
576 allocation: 1024 * 2,
577 allocation_count: 2,
578 deallocation: 0,
579 deallocation_count: 0,
580 category: Category::Unknown,
581 children: vec![],
582 }],
583 }],
584 }
585 );
586 }
587
588 #[test]
589 fn test_tree_value_3() {
590 let stats = Stats {
591 deallocations: VecDeque::new(),
592 allocations: VecDeque::from([
593 Allocation {
594 allocation_size: 1024,
595 deallocation_size: 0,
596 address: 0,
597 stack: VecDeque::from([
598 FrameInfo {
599 filename: Some("foo.rs".into()),
600 colno: Some(1),
601 lineno: Some(1),
602 fn_address: Some(std::ptr::null_mut()),
603 fn_name: Some("foo".into()),
604 },
605 FrameInfo {
606 filename: Some("foo2.rs".into()),
607 colno: Some(1),
608 lineno: Some(1),
609 fn_address: Some(std::ptr::null_mut()),
610 fn_name: Some("foo2".into()),
611 },
612 FrameInfo {
613 filename: Some("foo3.rs".into()),
614 colno: Some(1),
615 lineno: Some(1),
616 fn_address: Some(std::ptr::null_mut()),
617 fn_name: Some("foo3".into()),
618 },
619 ]),
620 },
621 Allocation {
622 allocation_size: 1024,
623 deallocation_size: 0,
624 address: 0,
625 stack: VecDeque::from([
626 FrameInfo {
627 filename: Some("foo.rs".into()),
628 colno: Some(1),
629 lineno: Some(1),
630 fn_address: Some(std::ptr::null_mut()),
631 fn_name: Some("foo".into()),
632 },
633 FrameInfo {
634 filename: Some("foo2.rs".into()),
635 colno: Some(1),
636 lineno: Some(1),
637 fn_address: Some(std::ptr::null_mut()),
638 fn_name: Some("foo2".into()),
639 },
640 FrameInfo {
641 filename: Some("foo3.rs".into()),
642 colno: Some(1),
643 lineno: Some(1),
644 fn_address: Some(std::ptr::null_mut()),
645 fn_name: Some("foo3".into()),
646 },
647 FrameInfo {
648 filename: Some("foo4.rs".into()),
649 colno: Some(1),
650 lineno: Some(1),
651 fn_address: Some(std::ptr::null_mut()),
652 fn_name: Some("foo4".into()),
653 },
654 ]),
655 },
656 ]),
657 };
658 let tree = stats.into_tree().unwrap();
659
660 assert_eq!(
661 tree,
662 Tree {
663 key: Key {
664 filename: "foo.rs".to_string(),
665 colno: 1,
666 lineno: 1,
667 fn_address: std::ptr::null_mut(),
668 fn_name: "foo".to_string(),
669 file_content: None,
670 },
671 allocation: 1024 * 2,
672 allocation_count: 2,
673 deallocation: 0,
674 deallocation_count: 0,
675 category: Category::Unknown,
676 children: vec![Tree {
677 key: Key {
678 filename: "foo2.rs".to_string(),
679 colno: 1,
680 lineno: 1,
681 fn_address: std::ptr::null_mut(),
682 fn_name: "foo2".to_string(),
683 file_content: None,
684 },
685 allocation: 1024 * 2,
686 allocation_count: 2,
687 deallocation: 0,
688 deallocation_count: 0,
689 category: Category::Unknown,
690 children: vec![Tree {
691 key: Key {
692 filename: "foo3.rs".to_string(),
693 colno: 1,
694 lineno: 1,
695 fn_address: std::ptr::null_mut(),
696 fn_name: "foo3".to_string(),
697 file_content: None,
698 },
699 allocation: 1024 * 2,
700 allocation_count: 2,
701 deallocation: 0,
702 deallocation_count: 0,
703 category: Category::Unknown,
704 children: vec![Tree {
705 key: Key {
706 filename: "foo4.rs".to_string(),
707 colno: 1,
708 lineno: 1,
709 fn_address: std::ptr::null_mut(),
710 fn_name: "foo4".to_string(),
711 file_content: None,
712 },
713 allocation: 1024,
714 allocation_count: 1,
715 deallocation: 0,
716 deallocation_count: 0,
717 category: Category::Unknown,
718 children: vec![],
719 }],
720 }],
721 }],
722 }
723 );
724 }
725
726 #[test]
727 fn test_tree_value_4() {
728 let stats = Stats {
729 deallocations: VecDeque::new(),
730 allocations: VecDeque::from([
731 Allocation {
732 allocation_size: 1024,
733 deallocation_size: 0,
734 address: 0,
735 stack: VecDeque::from([
736 FrameInfo {
737 filename: Some("foo.rs".into()),
738 colno: Some(1),
739 lineno: Some(1),
740 fn_address: Some(std::ptr::null_mut()),
741 fn_name: Some("foo".into()),
742 },
743 FrameInfo {
744 filename: Some("foo2.rs".into()),
745 colno: Some(1),
746 lineno: Some(1),
747 fn_address: Some(std::ptr::null_mut()),
748 fn_name: Some("foo2".into()),
749 },
750 FrameInfo {
751 filename: Some("foo3.rs".into()),
752 colno: Some(1),
753 lineno: Some(1),
754 fn_address: Some(std::ptr::null_mut()),
755 fn_name: Some("foo3".into()),
756 },
757 ]),
758 },
759 Allocation {
760 allocation_size: 1024,
761 deallocation_size: 0,
762 address: 0,
763 stack: VecDeque::from([
764 FrameInfo {
765 filename: Some("foo.rs".into()),
766 colno: Some(1),
767 lineno: Some(1),
768 fn_address: Some(std::ptr::null_mut()),
769 fn_name: Some("foo".into()),
770 },
771 FrameInfo {
772 filename: Some("foo2.rs".into()),
773 colno: Some(1),
774 lineno: Some(1),
775 fn_address: Some(std::ptr::null_mut()),
776 fn_name: Some("foo2".into()),
777 },
778 FrameInfo {
779 filename: Some("foo4.rs".into()),
780 colno: Some(1),
781 lineno: Some(1),
782 fn_address: Some(std::ptr::null_mut()),
783 fn_name: Some("foo4".into()),
784 },
785 ]),
786 },
787 ]),
788 };
789 let tree = stats.into_tree().unwrap();
790
791 assert_eq!(
792 tree,
793 Tree {
794 key: Key {
795 filename: "foo.rs".to_string(),
796 colno: 1,
797 lineno: 1,
798 fn_address: std::ptr::null_mut(),
799 fn_name: "foo".to_string(),
800 file_content: None,
801 },
802 allocation: 1024 * 2,
803 allocation_count: 2,
804 deallocation: 0,
805 deallocation_count: 0,
806 category: Category::Unknown,
807 children: vec![Tree {
808 key: Key {
809 filename: "foo2.rs".to_string(),
810 colno: 1,
811 lineno: 1,
812 fn_address: std::ptr::null_mut(),
813 fn_name: "foo2".to_string(),
814 file_content: None,
815 },
816 allocation: 1024 * 2,
817 allocation_count: 2,
818 deallocation: 0,
819 deallocation_count: 0,
820 category: Category::Unknown,
821 children: vec![
822 Tree {
823 key: Key {
824 filename: "foo4.rs".to_string(),
825 colno: 1,
826 lineno: 1,
827 fn_address: std::ptr::null_mut(),
828 fn_name: "foo4".to_string(),
829 file_content: None,
830 },
831 allocation: 1024,
832 allocation_count: 1,
833 deallocation: 0,
834 deallocation_count: 0,
835 category: Category::Unknown,
836 children: vec![],
837 },
838 Tree {
839 key: Key {
840 filename: "foo3.rs".to_string(),
841 colno: 1,
842 lineno: 1,
843 fn_address: std::ptr::null_mut(),
844 fn_name: "foo3".to_string(),
845 file_content: None,
846 },
847 allocation: 1024,
848 allocation_count: 1,
849 deallocation: 0,
850 deallocation_count: 0,
851 category: Category::Unknown,
852 children: vec![],
853 }
854 ],
855 }],
856 }
857 );
858 }
859}