1use crate::parser::*;
16use ratatui::{
17 buffer::Buffer,
18 layout::{Alignment, Constraint, Direction, Layout, Rect, Size},
19 style::{palette::tailwind, Color, Style, Stylize},
20 text::{Line, Span, Text},
21 widgets::{
22 Block, Cell, HighlightSpacing, Paragraph, Row, StatefulWidget, Table, TableState, Tabs,
23 Widget, Wrap
24 },
25};
26use tui_scrollview::{ScrollView, ScrollViewState};
27use rayon::prelude::*;
28use std::collections::HashMap;
29use std::error;
30use std::io;
31use std::time::Instant;
32
33use strum::IntoEnumIterator;
34use strum_macros::{Display, EnumIter, FromRepr};
35
36pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
38
39pub struct App<'a> {
41 pub header: String,
43 pub state: AppState,
44 pub selected_tab: SelectedTab,
45
46 pub parser: parser::CDParser,
48 pub filepath: String,
49 pub crash_dump: types::CrashDump,
50 pub index_map: IndexMap,
51 pub ancestor_map: HashMap<String, Vec<String>>,
52
53
54
55 pub tab_lists: HashMap<SelectedTab, Vec<String>>,
57 pub tab_rows: HashMap<SelectedTab, Vec<Row<'a>>>,
58
59 pub inspecting_pid: String,
60 pub inspect_scroll_state: ScrollViewState,
61
62 pub table_states: HashMap<SelectedTab, TableState>,
63
64 pub process_group_table: Table<'a>,
65
66 pub process_view_table: Table<'a>,
67 pub process_view_state: ProcessViewState,
68
69 pub footer_text: HashMap<SelectedTab, String>,
70}
71
72#[derive(Default, Clone, Copy, PartialEq, Eq)]
73pub enum AppState {
74 #[default]
75 Running,
76 Quitting,
77}
78
79#[derive(Default, Clone, Copy, PartialEq, Eq)]
80pub enum ProcessViewState {
81 Heap,
82 #[default]
83 Stack,
84 MessageQueue,
85}
86
87#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter, PartialEq, Eq, Hash)]
88pub enum SelectedTab {
89 #[default]
90 #[strum(to_string = "General Information")]
91 General,
92 #[strum(to_string = "Process Group Info")]
95 ProcessGroup,
96 #[strum(to_string = "Process Info")]
97 Process,
98 #[strum(to_string = "Inspector")]
99 Inspect,
100}
101
102impl Default for App<'_> {
103 fn default() -> Self {
104 Self {
105 state: AppState::Running,
106 selected_tab: SelectedTab::General,
107 parser: parser::CDParser::default(),
108 filepath: "".to_string(),
109 crash_dump: types::CrashDump::new(),
110 index_map: IndexMap::new(),
111 ancestor_map: HashMap::new(),
112 header: "ERL CRASH DUMP VIEWER".to_string(),
113 tab_lists: HashMap::from_iter(SelectedTab::iter().map(|tab| (tab, vec![]))),
114 tab_rows: HashMap::from_iter(SelectedTab::iter().map(|tab| (tab, vec![]))),
115 table_states: HashMap::from_iter(
116 SelectedTab::iter().map(|tab| (tab, TableState::default())),
117 ),
118 process_group_table: Table::default(),
119 process_view_state: ProcessViewState::default(),
120 process_view_table: Table::default(),
121 footer_text: HashMap::new(),
122 inspecting_pid: "".to_string(),
123 inspect_scroll_state: ScrollViewState::default(),
124 }
125 }
126}
127
128impl App<'_> {
129 pub fn new(filepath: String) -> Self {
131 let now = Instant::now();
132
133 let parser = parser::CDParser::new(&filepath).unwrap();
134
135 let mut ret = Self::default();
136 ret.filepath = filepath.clone();
137
138 ret.process_view_state = ProcessViewState::default();
139
140 ret.index_map = parser.build_index().unwrap();
148 ret.crash_dump = parser.parse(&ret.index_map).unwrap();
149
150 ret.ancestor_map = parser::CDParser::create_descendants_table(&ret.crash_dump.processes);
154 let group_info =
156 parser::CDParser::calculate_group_info(&ret.ancestor_map, &ret.crash_dump.processes);
157 ret.crash_dump.group_info_map = group_info;
158 let read_only_processes = ret.crash_dump.processes.clone().into_read_only();
199 let mut sorted_keys: Vec<(&String, &InfoOrIndex<ProcInfo>)> =
200 read_only_processes.iter().collect();
201 sorted_keys.par_sort_by(|a, b| match (a.1, b.1) {
202 (InfoOrIndex::Info(proc_info_a), InfoOrIndex::Info(proc_info_b)) => {
203 proc_info_b.bin_vheap.cmp(&proc_info_a.bin_vheap)
204 }
205 _ => unreachable!(),
206 });
207 let sorted_key_list: Vec<String> = sorted_keys
208 .into_par_iter() .map(|(key, _)| key.clone())
210 .collect();
211 ret.tab_lists.get_mut(&SelectedTab::Process).map(|val| {
212 *val = sorted_key_list;
213 });
214
215 let process_rows: Vec<Row> = ret.tab_lists[&SelectedTab::Process]
216 .par_iter() .map(|pid| {
218 match ret.crash_dump.processes.get(pid) {
219 Some(process_ref) => {
220 match *process_ref.value() {
221 InfoOrIndex::Info(ref proc_info) => {
223 let item = proc_info.ref_array();
224 Row::new(item)
225 }
226 _ => {
227 Row::new(vec![format!("Unexpected Index for pid: {:?}", pid)])
229 }
231 }
232 }
233 None => {
234 Row::new(vec![format!("Process not found: {:?}", pid)]) }
237 }
238 })
239 .collect();
240
241 let selected_row_style = Style::default().fg(Color::White).bg(Color::Blue);
242 let selected_col_style = Style::default().fg(Color::White);
243 let selected_cell_style = Style::default().fg(Color::White);
244 let header_style = Style::default().fg(Color::White).bg(Color::Red);
245
246 let process_header = ProcInfo::headers()
247 .into_iter()
248 .map(Cell::from)
249 .collect::<Row>()
250 .style(header_style)
251 .height(1);
252
253 ret.process_view_table = Table::new(
254 process_rows,
255 [
256 Constraint::Length(15),
257 Constraint::Length(25),
258 Constraint::Length(25),
259 Constraint::Length(25),
260 Constraint::Length(25),
261 Constraint::Length(25),
262 Constraint::Length(25),
263 Constraint::Length(25),
264 Constraint::Length(25),
265 ],
266 )
267 .header(process_header)
268 .row_highlight_style(selected_row_style)
269 .column_highlight_style(selected_col_style)
270 .cell_highlight_style(selected_cell_style)
271 .highlight_spacing(HighlightSpacing::Always)
272 .block(Block::bordered().title(SelectedTab::Process.to_string()));
273
274 let mut sorted_keys: Vec<(&String, &GroupInfo)> = ret
277 .crash_dump
278 .group_info_map
279 .par_iter() .collect();
281 sorted_keys.par_sort_by(|a, b| b.1.total_memory_size.cmp(&a.1.total_memory_size));
282 let sorted_key_list: Vec<String> = sorted_keys
283 .into_par_iter() .map(|(key, _)| key.clone())
285 .collect();
286 ret.tab_lists
287 .get_mut(&SelectedTab::ProcessGroup)
288 .map(|val| {
289 *val = sorted_key_list;
290 });
291 let process_group_rows: Vec<Row> = ret.tab_lists[&SelectedTab::ProcessGroup]
292 .par_iter() .map(|group| {
294 let group_info = ret.crash_dump.group_info_map.get(group).unwrap();
295 let item = group_info.ref_array();
296 Row::new(item)
297 })
298 .collect();
299
300 let process_group_headers = GroupInfo::headers()
301 .into_iter()
302 .map(Cell::from)
303 .collect::<Row>()
304 .style(header_style)
305 .height(1);
306
307 ret.process_group_table = Table::new(
308 process_group_rows,
309 [
310 Constraint::Length(30),
311 Constraint::Length(30),
312 Constraint::Length(30),
313 Constraint::Length(30),
314 ],
315 )
316 .header(process_group_headers)
317 .row_highlight_style(selected_row_style)
318 .column_highlight_style(selected_col_style)
319 .cell_highlight_style(selected_cell_style)
320 .highlight_spacing(HighlightSpacing::Always)
321 .block(Block::bordered().title(SelectedTab::Process.to_string()));
322
323 ret.footer_text.insert(SelectedTab::Process, "Press S for Stack, H for Heap, M for Message Queue | I to inspect contents | < > to change tabs | Press q to quit".to_string());
324 ret.footer_text.insert(SelectedTab::Inspect, "Press I to return to process info | < > to change tabs | q to quit".to_string());
325
326 if let Some(state) = ret.table_states.get_mut(&SelectedTab::Process) {
333 if !ret.tab_lists[&SelectedTab::Process].is_empty() {
334 state.select(Some(0));
335 }
336 }
337
338 if let Some(state) = ret.table_states.get_mut(&SelectedTab::ProcessGroup) {
339 if !ret.tab_lists[&SelectedTab::ProcessGroup].is_empty() {
340 state.select(Some(0));
341 }
342 }
343
344
345 ret.inspect_scroll_state = ScrollViewState::default();
346
347 let elapsed = now.elapsed();
348 println!("Building everything took: {:.2?}", elapsed);
349
350 ret
351 }
352
353 pub fn tick(&self) {}
355
356 pub fn quit(&mut self) {
358 self.state = AppState::Quitting;
359 }
360
361 pub fn next_tab(&mut self) {
362 self.selected_tab = self.selected_tab.next()
363 }
364
365 pub fn prev_tab(&mut self) {
366 self.selected_tab = self.selected_tab.previous()
367 }
368
369 pub fn get_heap_info(&self, pid: &str) -> io::Result<Text> {
370 self.parser
371 .get_heap_info(&self.crash_dump, &self.filepath, pid)
372 }
373
374 pub fn get_stack_info(&self, pid: &str) -> io::Result<Text> {
375 self.parser
376 .get_stack_info(&self.crash_dump, &self.filepath, pid)
377 }
378
379 pub fn get_message_queue_info(&self, pid: &str) -> io::Result<Text> {
380 self.parser
381 .get_message_queue_info(&self.crash_dump, &self.filepath, pid)
382 }
383}
384
385impl App<'_> {
387 pub fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
388 let titles = SelectedTab::iter().map(SelectedTab::title);
389 let highlight_style = (Color::default(), self.selected_tab.palette().c700);
390 let selected_tab_index = self.selected_tab as usize;
391 Tabs::new(titles)
392 .highlight_style(highlight_style)
393 .select(selected_tab_index)
394 .padding("", "")
395 .divider(" ")
396 .render(area, buf);
397 }
398
399 pub fn get_selected_pid(&self) -> String {
400 if self.selected_tab == SelectedTab::Process {
401 let process_table_state = self.table_states.get(&SelectedTab::Process).unwrap();
402 let selected_item = process_table_state.selected().unwrap_or(0);
403 self.tab_lists[&SelectedTab::Process][selected_item].clone()
404 } else {
405 String::new()
406 }
407 }
408}
409
410impl Widget for &mut App<'_> {
411 fn render(self, area: Rect, buf: &mut Buffer) {
412 use Constraint::{Length, Min};
413 let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
414 let [header_area, inner_area, footer_area] = vertical.areas(area);
415
416 let horizontal = Layout::horizontal([Min(0), Length(20)]);
417 let [tabs_area, title_area] = horizontal.areas(header_area);
418
419 render_title(title_area, buf);
420 self.render_tabs(tabs_area, buf);
421 match self.selected_tab {
422 SelectedTab::General => self.selected_tab.render_general(inner_area, buf, self),
423 SelectedTab::Process => self.selected_tab.render_process(inner_area, buf, self),
425 SelectedTab::ProcessGroup => self
426 .selected_tab
427 .render_process_group(inner_area, buf, self),
428 SelectedTab::Inspect => self.selected_tab.render_inspect(inner_area, buf, self),
429 }
430 let footer_text = self
431 .footer_text
432 .get(&self.selected_tab)
433 .map_or("< > to change tabs | Press q to quit", |v| v);
434 render_footer(footer_text, footer_area, buf);
435 }
436}
437
438impl SelectedTab {
439 fn previous(self) -> Self {
441 let current_index: usize = self as usize;
442 let previous_index = current_index.saturating_sub(1);
443 Self::from_repr(previous_index).unwrap_or(self)
444 }
445
446 fn next(self) -> Self {
448 let current_index = self as usize;
449 let next_index = current_index.saturating_add(1);
450 Self::from_repr(next_index).unwrap_or(self)
451 }
452}
453
454impl SelectedTab {
455 fn title(self) -> Line<'static> {
457 format!(" {self} ")
458 .fg(tailwind::SLATE.c200)
459 .bg(self.palette().c900)
460 .into()
461 }
462
463 fn render_general(self, area: Rect, buf: &mut Buffer, app: &mut App) {
464 let preamble_text = app.crash_dump.preamble.format();
465 let process_count = app.index_map[&Tag::Proc].len();
466 let ets_count = app.index_map[&Tag::Ets].len();
467 let fn_count = app.index_map[&Tag::Fun].len();
468
469 let memory_info_text = app.crash_dump.memory.format();
470
471 let preamble_lines: Vec<Line> = preamble_text
473 .lines()
474 .map(|line| Line::from(Span::styled(line, Style::default().fg(Color::White))))
475 .collect();
476
477 let memory_information_lines: Vec<Line> = memory_info_text
479 .lines()
480 .map(|line| Line::from(Span::styled(line, Style::default().fg(Color::White))))
481 .collect();
482
483 let memory_information_header = Line::from(vec![
485 Span::styled("Memory Information:", Style::default().fg(Color::Yellow)),
486 Span::raw("\n"),
487 ]);
488
489 let process_count = Line::from(vec![
490 Span::styled("Process Count: ", Style::default().fg(Color::Cyan)),
491 Span::styled(process_count.to_string(), Style::default().fg(Color::White)),
492 ]);
493
494 let ets_count = Line::from(vec![
495 Span::styled("ETS Tables: ", Style::default().fg(Color::Cyan)),
496 Span::styled(ets_count.to_string(), Style::default().fg(Color::White)),
497 ]);
498
499 let fn_count = Line::from(vec![
500 Span::styled("Funs: ", Style::default().fg(Color::Cyan)),
501 Span::styled(fn_count.to_string(), Style::default().fg(Color::White)),
502 ]);
503
504 let mut general_info_text = Text::from(preamble_lines);
506 general_info_text.extend(vec![memory_information_header]);
507 general_info_text.extend(memory_information_lines);
508 general_info_text.extend(process_count);
509 general_info_text.extend(ets_count);
510 general_info_text.extend(fn_count);
511
512 let paragraph = Paragraph::new(general_info_text)
513 .block(Block::bordered().title("General Information"))
514 .style(Style::default().fg(Color::White))
515 .alignment(Alignment::Left);
516
517 Widget::render(¶graph, area, buf);
518 }
519
520 fn render_process(self, area: Rect, buf: &mut Buffer, app: &mut App) {
538 let outer_layout = Layout::default()
539 .direction(Direction::Vertical)
540 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
541 .split(area);
542
543 let inner_layout = Layout::default()
545 .direction(Direction::Horizontal)
546 .constraints(vec![Constraint::Percentage(25), Constraint::Percentage(75)])
547 .split(outer_layout[1]);
548
549 let selected_item;
550 {
551 let process_table_state = app.table_states.get_mut(&SelectedTab::Process).unwrap();
552 selected_item = process_table_state.selected().unwrap_or(0);
553 StatefulWidget::render(
554 &app.process_view_table,
555 outer_layout[0],
556 buf,
557 process_table_state,
558 );
559 }
560
561 let selected_pid = &app.tab_lists[&SelectedTab::Process][selected_item];
562 let selected_process_result = app.crash_dump.processes.get(selected_pid);
563
564 let active_proc_info: types::ProcInfo;
565 let process_info_text: Text;
566 match selected_process_result {
567 Some(process_ref) => {
568 let text = match *process_ref.value() {
569 InfoOrIndex::Info(ref proc_info) => {
570 let proc_info: &types::ProcInfo = proc_info;
571 active_proc_info = proc_info.clone();
572 active_proc_info.format_as_ratatui_text()
573 }
574 InfoOrIndex::Index(_) => {
575 Text::raw(format!("Index for pid: {:?}", selected_pid).to_string())
576 }
577 };
578 process_info_text = text;
579 }
580 None => {
581 process_info_text =
582 Text::raw(format!("Process not found: {:?}", selected_pid).to_string());
583 }
584 };
585
586 let (inspect_info_title, inspect_info_text) = match app.process_view_state {
587 ProcessViewState::Stack => {
588 app.inspecting_pid = selected_pid.clone();
589 ("Decoded Stack", app.get_stack_info(selected_pid).unwrap())
590 }
591 ProcessViewState::Heap => {
592 app.inspecting_pid = selected_pid.clone();
593
594 ("Decoded Heap", app.get_heap_info(selected_pid).unwrap())
595 }
596 ProcessViewState::MessageQueue => {
597 app.inspecting_pid = selected_pid.clone();
598 (
599 "Decoded Message Queue",
600 app.get_message_queue_info(selected_pid).unwrap(),
601 )}
602 };
603
604 let detail_block = Paragraph::new(process_info_text)
607 .block(Block::bordered().title("Process Details"))
608 .style(Style::default().fg(Color::White))
609 .wrap(Wrap { trim: false })
610 .alignment(Alignment::Left);
611
612 let proc_heap = Paragraph::new(inspect_info_text)
613 .block(Block::bordered().title(inspect_info_title))
614 .style(Style::default().fg(Color::White))
615 .alignment(Alignment::Left);
616
617 Widget::render(&detail_block, inner_layout[0], buf);
618 Widget::render(&proc_heap, inner_layout[1], buf);
619 }
620
621 fn render_process_group(self, area: Rect, buf: &mut Buffer, app: &mut App) {
622 let outer_layout = Layout::default()
623 .direction(Direction::Horizontal)
624 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
625 .split(area);
626
627 let inner_layout = Layout::default()
629 .direction(Direction::Vertical)
630 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
631 .split(outer_layout[1]);
632
633 let group_table_state = app
634 .table_states
635 .get_mut(&SelectedTab::ProcessGroup)
636 .unwrap();
637
638 let selected_item = group_table_state.selected().unwrap_or(0);
639 let selected_pid = &app.tab_lists[&SelectedTab::ProcessGroup][selected_item];
640 let selected_process_result = app.crash_dump.processes.get(selected_pid);
641
642 let active_proc_info: types::ProcInfo;
643 let process_info_text: Text;
644 match selected_process_result {
645 Some(process_ref) => {
646 let text = match *process_ref.value() {
647 InfoOrIndex::Info(ref proc_info) => {
648 let proc_info: &types::ProcInfo = proc_info;
649 active_proc_info = proc_info.clone();
650 active_proc_info.format_as_ratatui_text()
651 }
652 InfoOrIndex::Index(_) => {
653 Text::raw(format!("Index for pid: {:?}", selected_pid).to_string())
654 }
655 };
656 process_info_text = text;
657 }
658 None => {
659 process_info_text =
660 Text::raw(format!("Process not found: {:?}", selected_pid).to_string());
661 }
662 };
663
664 let children: Vec<Row> = match app.ancestor_map.get(selected_pid) {
665 Some(child_pids) => {
666 child_pids
667 .iter() .map(|child_pid| {
669 match app.crash_dump.processes.get(child_pid) {
670 Some(child_info_ref) => {
671 match *child_info_ref.value() {
672 InfoOrIndex::Info(ref proc_info) => {
674 Row::new(proc_info.summary_ref_array())
675 }
676 InfoOrIndex::Index(_) => {
677 Row::new(vec![format!("{:?}", child_pid)])
678 } }
680 }
681 None => {
682 Row::new(vec![format!("Info not found: {:?}", child_pid)])
684 }
685 }
686 })
687 .collect()
688 }
689 None => vec![Row::new(vec!["No data".to_string()])],
690 };
691
692 let children_block = Table::new(
694 children,
695 [
696 Constraint::Length(15),
697 Constraint::Length(60),
698 Constraint::Length(10),
699 Constraint::Length(20),
700 Constraint::Length(25),
701 ],
702 )
703 .header(
704 ["Pid", "Name", "Memory", "Reductions", "MsgQ Length"]
705 .iter()
706 .map(|&h| Cell::from(h))
707 .collect::<Row>()
708 .style(Style::default().fg(Color::White).bg(Color::Green)),
709 )
710 .highlight_spacing(HighlightSpacing::Always)
711 .block(Block::bordered().title("Group Children"));
712
713 let detail_block = Paragraph::new(process_info_text)
714 .block(Block::bordered().title("Ancestor Details"))
715 .style(Style::default().fg(Color::White))
716 .wrap(Wrap { trim: false })
717 .alignment(Alignment::Left);
718
719 Widget::render(&children_block, inner_layout[0], buf);
720 Widget::render(&detail_block, inner_layout[1], buf);
721 StatefulWidget::render(
722 &app.process_group_table,
723 outer_layout[0],
724 buf,
725 group_table_state,
726 );
727 }
728
729 fn render_inspect(self, area: Rect, buf: &mut Buffer, app: &mut App) {
730 let width = if buf.area.height < 70 {
731 buf.area.width - 1
732 } else {
733 buf.area.width
734 };
735 let mut scroll_view = ScrollView::new(Size::new(width, 70));
736
737 let inspect_info_text;
738 let inspect_info_title;
739 {
740 let (t1, t2) = match app.process_view_state {
741 ProcessViewState::Stack =>
742 {
743 ("Decoded Stack", app.get_stack_info(&app.inspecting_pid).unwrap())
744 },
745 ProcessViewState::Heap => ("Decoded Heap", app.get_heap_info(&app.inspecting_pid).unwrap()),
746 ProcessViewState::MessageQueue => (
747 "Decoded Message Queue",
748 app.get_message_queue_info(&app.inspecting_pid).unwrap(),
749 ),
750 };
751 inspect_info_title = t1;
752 inspect_info_text = t2.clone();
753 }
754
755 let proc_info = Paragraph::new(inspect_info_text)
756 .block(Block::bordered().title(inspect_info_title))
757 .style(Style::default().fg(Color::White))
758 .wrap(Wrap { trim: false })
759 .alignment(Alignment::Left);
760
761
762 proc_info.render(area, &mut scroll_view.buf_mut());
763 scroll_view.render(area, buf, &mut app.inspect_scroll_state);
764 }
765
766 const fn palette(self) -> tailwind::Palette {
767 match self {
768 Self::General => tailwind::BLUE,
769 Self::Process => tailwind::EMERALD,
771 Self::ProcessGroup => tailwind::INDIGO,
772 Self::Inspect => tailwind::PURPLE,
773 }
774 }
775}
776
777fn render_title(area: Rect, buf: &mut Buffer) {
778 "ERL Crash Dump".render(area, buf);
779}
780
781fn render_footer(footer_text: &str, area: Rect, buf: &mut Buffer) {
782 Line::raw(footer_text).centered().render(area, buf);
783}