1use crate::config::CommonColors;
16use crate::parser::*;
17use ratatui::{
18 buffer::Buffer,
19 layout::{Alignment, Constraint, Direction, Layout, Rect, Size},
20 style::{palette::tailwind, Color, Style, Stylize},
21 text::{Line, Span, Text},
22 widgets::{
23 Block, Cell, Clear, HighlightSpacing, Paragraph, Row, StatefulWidget, Table, TableState,
24 Tabs, Widget, Wrap,
25 },
26};
27use rayon::prelude::*;
28use std::collections::HashMap;
29use std::error;
30use std::io;
31use std::time::Instant;
32use tui_scrollview::{ScrollView, ScrollViewState};
33
34use strum::IntoEnumIterator;
35use strum_macros::{Display, EnumIter, FromRepr};
36
37pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
39
40pub struct App<'a> {
42 pub header: String,
44 pub state: AppState,
45 pub selected_tab: SelectedTab,
46
47 pub parser: parser::CDParser,
49 pub filepath: String,
50 pub crash_dump: types::CrashDump,
51 pub index_map: IndexMap,
52 pub ancestor_map: HashMap<String, Vec<String>>,
53
54 pub tab_lists: HashMap<SelectedTab, Vec<String>>,
56 pub tab_rows: HashMap<SelectedTab, Vec<Row<'a>>>,
57
58 pub inspecting_pid: String,
59 pub inspect_scroll_state: ScrollViewState,
60
61 pub table_states: HashMap<SelectedTab, TableState>,
62
63 pub process_group_table: Table<'a>,
64 pub process_group_sort_column: ProcessGroupSortColumn,
65 pub process_group_sort_direction: SortDirection,
66
67 pub process_view_table: Table<'a>,
68 pub process_view_state: ProcessViewState,
69 pub process_sort_column: ProcessSortColumn,
70 pub process_sort_direction: SortDirection,
71 pub process_readonly_view: Option<dashmap::ReadOnlyView<String, InfoOrIndex<ProcInfo>>>,
72
73 pub footer_text: HashMap<SelectedTab, String>,
74
75 pub colors: CommonColors,
76 pub show_help: bool,
77}
78
79#[derive(Default, Clone, Copy, PartialEq, Eq)]
80pub enum AppState {
81 #[default]
82 Running,
83 Quitting,
84}
85
86#[derive(Default, Clone, Copy, PartialEq, Eq)]
87pub enum ProcessViewState {
88 Heap,
89 #[default]
90 Stack,
91 MessageQueue,
92}
93
94#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter, PartialEq, Eq, Hash)]
95pub enum SelectedTab {
96 #[default]
97 #[strum(to_string = "General Information")]
98 General,
99 #[strum(to_string = "Process Group Info")]
100 ProcessGroup,
101 #[strum(to_string = "Process Info")]
102 Process,
103 #[strum(to_string = "Inspector")]
104 Inspect,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum ProcessSortColumn {
109 Pid = 0,
110 Name = 1,
111 MessageQueueLength = 2,
112 Memory = 3,
113 TotalBinVHeap = 4,
114 BinVHeap = 5,
115 BinVHeapUnused = 6,
116 OldBinVHeap = 7,
117 OldBinVHeapUnused = 8,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ProcessGroupSortColumn {
122 Pid = 0,
123 Name = 1,
124 TotalMemorySize = 2,
125 ChildrenCount = 3,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum SortDirection {
130 Ascending,
131 Descending,
132}
133
134impl Default for App<'_> {
135 fn default() -> Self {
136 Self {
137 state: AppState::Running,
138 selected_tab: SelectedTab::General,
139 parser: parser::CDParser::default(),
140 filepath: "".to_string(),
141 crash_dump: types::CrashDump::new(),
142 index_map: IndexMap::new(),
143 ancestor_map: HashMap::new(),
144 header: "ERL CRASH DUMP VIEWER".to_string(),
145 tab_lists: HashMap::from_iter(SelectedTab::iter().map(|tab| (tab, vec![]))),
146 tab_rows: HashMap::from_iter(SelectedTab::iter().map(|tab| (tab, vec![]))),
147 table_states: HashMap::from_iter(
148 SelectedTab::iter().map(|tab| (tab, TableState::default())),
149 ),
150 process_group_table: Table::default(),
151 process_group_sort_column: ProcessGroupSortColumn::TotalMemorySize,
152 process_group_sort_direction: SortDirection::Descending,
153
154 process_view_state: ProcessViewState::default(),
155 process_view_table: Table::default(),
156 process_sort_column: ProcessSortColumn::BinVHeap,
157 process_sort_direction: SortDirection::Descending,
158 process_readonly_view: None,
159 footer_text: HashMap::new(),
160 inspecting_pid: "".to_string(),
161 inspect_scroll_state: ScrollViewState::default(),
162 colors: CommonColors::default(),
163 show_help: false,
164 }
165 }
166}
167
168impl App<'_> {
169
170 pub fn new(filepath: String, colors: Option<CommonColors>) -> Result<Self, io::Error> {
171 let now = Instant::now();
172
173 let parser = parser::CDParser::new(&filepath)?;
174
175 let mut ret = Self::default();
176
177 ret.filepath = filepath.clone();
178 if let Some(colors) = colors {
179 ret.colors = colors;
180 }
181
182 ret.index_map = parser.build_index()?;
183 if ret.index_map.get(&Tag::Proc) == None {
184 println!("Couldn't find any processes! Are you sure this crash dump is valid?");
185 return Err(io::Error::other("Invalid crash dump detected. Found no `proc` sections."));
186 }
187 ret.crash_dump = parser.parse(&ret.index_map)?;
188
189 ret.ancestor_map = parser::CDParser::create_descendants_table(&ret.crash_dump.processes);
190 ret.crash_dump.group_info_map =
191 parser::CDParser::calculate_group_info(&ret.ancestor_map, &ret.crash_dump.processes);
192
193 let read_only_processes = ret.crash_dump.processes.clone().into_read_only();
194 ret.process_readonly_view = Some(read_only_processes);
195
196 ret.sort_and_update_process_table();
197 ret.sort_and_update_process_group_table();
198
199 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());
202 ret.footer_text.insert(
203 SelectedTab::Inspect,
204 " <-, -> to change tabs | q to quit | ? for help".to_string(),
205 );
206
207 if let Some(state) = ret.table_states.get_mut(&SelectedTab::Process) {
210 if !ret.tab_lists[&SelectedTab::Process].is_empty() {
211 state.select(Some(0));
212 }
213 }
214
215 if let Some(state) = ret.table_states.get_mut(&SelectedTab::ProcessGroup) {
216 if !ret.tab_lists[&SelectedTab::ProcessGroup].is_empty() {
217 state.select(Some(0));
218 }
219 }
220
221 ret.inspect_scroll_state = ScrollViewState::default();
222
223 let elapsed = now.elapsed();
224 println!("Building everything took: {:.2?}", elapsed);
225
226 Ok(ret)
227 }
228
229 pub fn sort_and_update_process_group_table(&mut self) {
230 let column = self.process_group_sort_column;
231 let direction = self.process_group_sort_direction;
232
233 let mut sorted_keys: Vec<(&String, &GroupInfo)> = self
234 .crash_dump
235 .group_info_map
236 .par_iter() .collect();
238 sorted_keys.par_sort_by(|a, b| match (a.1, b.1) {
239 (grp_a, grp_b) => match direction {
240 SortDirection::Ascending => match column {
241 ProcessGroupSortColumn::Pid => grp_a.pid.cmp(&grp_b.pid),
242 ProcessGroupSortColumn::Name => grp_a.name.cmp(&grp_b.name),
243 ProcessGroupSortColumn::ChildrenCount => grp_a.children.len().cmp(&grp_b.children.len()),
244 ProcessGroupSortColumn::TotalMemorySize => {
245 grp_a.total_memory_size.cmp(&grp_b.total_memory_size)
246 }
247 },
248 SortDirection::Descending => match column {
249 ProcessGroupSortColumn::Pid => grp_b.pid.cmp(&grp_a.pid),
250 ProcessGroupSortColumn::Name => grp_b.name.cmp(&grp_a.name),
251 ProcessGroupSortColumn::ChildrenCount => grp_b.children.len().cmp(&grp_a.children.len()),
252 ProcessGroupSortColumn::TotalMemorySize => {
253 grp_b.total_memory_size.cmp(&grp_a.total_memory_size)
254 }
255 },
256 },
257 });
258
259 let selected_row_style = Style::default()
260 .fg(self.colors.highlight_text)
261 .bg(self.colors.highlight_background);
262 let header_style = Style::default()
263 .fg(self.colors.header_text)
264 .bg(self.colors.header_background);
265
266 let sorted_key_list: Vec<String> = sorted_keys
267 .into_par_iter() .map(|(key, _)| key.clone())
269 .collect();
270 self.tab_lists
271 .get_mut(&SelectedTab::ProcessGroup)
272 .map(|val| {
273 *val = sorted_key_list;
274 });
275 let process_group_rows: Vec<Row> = self.tab_lists[&SelectedTab::ProcessGroup]
276 .par_iter()
277 .map(|group| {
278 let group_info = self.crash_dump.group_info_map.get(group).unwrap();
279 let item = group_info.ref_array();
280 Row::new(item)
281 })
282 .collect();
283
284 let header_cells = GroupInfo::headers()
285 .iter()
286 .enumerate()
287 .map(|(i, &h)| {
288 let indicator = if i == self.process_group_sort_column as usize {
289 match self.process_group_sort_direction {
290 SortDirection::Ascending => " ▲",
291 SortDirection::Descending => " ▼",
292 }
293 } else {
294 ""
295 };
296 Cell::from(format!("{}{}", h, indicator))
297 })
298 .collect::<Vec<_>>();
299
300 let process_group_header = Row::new(header_cells).style(header_style).height(1);
301
302 self.process_group_table = Table::new(
303 process_group_rows,
304 [
305 Constraint::Length(30),
306 Constraint::Length(30),
307 Constraint::Length(30),
308 Constraint::Length(30),
309 ],
310 )
311 .header(process_group_header)
312 .row_highlight_style(selected_row_style)
313 .highlight_spacing(HighlightSpacing::Always)
314 .block(Block::bordered().title(SelectedTab::Process.to_string()));
315 }
316
317 pub fn sort_and_update_process_table(&mut self) {
318 let column = self.process_sort_column;
319 let direction = self.process_sort_direction;
320 let processes_map = &self.crash_dump.processes;
321
322 let mut pids_to_sort: Vec<(&String, &InfoOrIndex<ProcInfo>)> = self
323 .process_readonly_view
324 .as_ref()
325 .unwrap()
326 .iter()
327 .collect();
328
329 pids_to_sort.par_sort_by(|a, b| match (a.1, b.1) {
330 (InfoOrIndex::Info(proc_a), InfoOrIndex::Info(proc_b)) => match direction {
331 SortDirection::Ascending => match column {
332 ProcessSortColumn::Pid => proc_a.pid.cmp(&proc_b.pid),
333 ProcessSortColumn::Name => proc_a.name.cmp(&proc_b.name),
334 ProcessSortColumn::Memory => proc_a.memory.cmp(&proc_b.memory),
335 ProcessSortColumn::TotalBinVHeap => {
336 proc_a.total_bin_vheap.cmp(&proc_b.total_bin_vheap)
337 }
338 ProcessSortColumn::BinVHeap => proc_a.bin_vheap.cmp(&proc_b.bin_vheap),
339 ProcessSortColumn::BinVHeapUnused => {
340 proc_a.bin_vheap_unused.cmp(&proc_b.bin_vheap_unused)
341 }
342 ProcessSortColumn::OldBinVHeap => {
343 proc_a.old_bin_vheap.cmp(&proc_b.old_bin_vheap)
344 }
345 ProcessSortColumn::OldBinVHeapUnused => proc_a
346 .old_bin_vheap_unused
347 .cmp(&proc_b.old_bin_vheap_unused),
348 ProcessSortColumn::MessageQueueLength => proc_a
349 .message_queue_length
350 .cmp(&proc_b.message_queue_length),
351 },
352 SortDirection::Descending => match column {
354 ProcessSortColumn::Pid => proc_b.pid.cmp(&proc_a.pid),
355 ProcessSortColumn::Name => proc_b.name.cmp(&proc_a.name),
356 ProcessSortColumn::Memory => proc_b.memory.cmp(&proc_a.memory),
357 ProcessSortColumn::TotalBinVHeap => {
358 proc_b.total_bin_vheap.cmp(&proc_a.total_bin_vheap)
359 }
360 ProcessSortColumn::BinVHeap => proc_b.bin_vheap.cmp(&proc_a.bin_vheap),
361 ProcessSortColumn::BinVHeapUnused => {
362 proc_b.bin_vheap_unused.cmp(&proc_a.bin_vheap_unused)
363 }
364 ProcessSortColumn::OldBinVHeap => {
365 proc_b.old_bin_vheap.cmp(&proc_a.old_bin_vheap)
366 }
367 ProcessSortColumn::OldBinVHeapUnused => proc_b
368 .old_bin_vheap_unused
369 .cmp(&proc_a.old_bin_vheap_unused),
370 ProcessSortColumn::MessageQueueLength => proc_b
371 .message_queue_length
372 .cmp(&proc_a.message_queue_length),
373 },
374 },
375 _ => unreachable!(),
376 });
377
378 let sorted_key_list: Vec<String> = pids_to_sort
379 .into_par_iter()
380 .map(|(key, _)| key.clone())
381 .collect();
382 self.tab_lists.get_mut(&SelectedTab::Process).map(|val| {
383 *val = sorted_key_list;
384 });
385
386 let process_rows: Vec<Row> = self.tab_lists[&SelectedTab::Process]
387 .par_iter()
388 .map(|pid| {
389 match processes_map.get(pid) {
390 Some(process_ref) => match *process_ref.value() {
391 InfoOrIndex::Info(ref proc_info) => {
392 Row::new(proc_info.ref_array())
394 }
395 _ => Row::new(vec![format!("Index: {}", pid)]),
396 },
397 None => Row::new(vec![format!("Not Found: {}", pid)]),
398 }
399 })
400 .collect();
401
402 let selected_row_style = Style::default()
403 .fg(self.colors.highlight_text)
404 .bg(self.colors.highlight_background);
405 let header_style = Style::default()
406 .fg(self.colors.header_text)
407 .bg(self.colors.header_background);
408
409 let header_cells = ProcInfo::headers()
410 .iter()
411 .enumerate()
412 .map(|(i, &h)| {
413 let indicator = if i == self.process_sort_column as usize {
414 match self.process_sort_direction {
415 SortDirection::Ascending => " ▲",
416 SortDirection::Descending => " ▼",
417 }
418 } else {
419 ""
420 };
421 Cell::from(format!("{}{}", h, indicator))
422 })
423 .collect::<Vec<_>>();
424
425 let process_header = Row::new(header_cells).style(header_style).height(1);
426
427 let constraints = vec![
428 Constraint::Length(25),
429 Constraint::Length(25),
430 Constraint::Length(25),
431 Constraint::Length(25),
432 Constraint::Length(25),
433 Constraint::Length(25),
434 Constraint::Length(25),
435 Constraint::Length(25),
436 Constraint::Length(25),
437 ];
438
439 self.process_view_table = Table::new(process_rows, constraints) .header(process_header)
441 .row_highlight_style(selected_row_style)
442 .highlight_spacing(HighlightSpacing::Always)
443 .block(Block::bordered().title(SelectedTab::Process.to_string()));
444
445 if let Some(state) = self.table_states.get_mut(&SelectedTab::Process) {
446 state.select(Some(0));
447 }
448 }
449
450 pub fn tick(&self) {}
452
453 pub fn quit(&mut self) {
455 self.state = AppState::Quitting;
456 }
457
458 pub fn next_tab(&mut self) {
459 self.selected_tab = self.selected_tab.next()
460 }
461
462 pub fn prev_tab(&mut self) {
463 self.selected_tab = self.selected_tab.previous()
464 }
465
466 pub fn get_heap_info(&self, pid: &str) -> io::Result<Text> {
467 self.parser
468 .get_heap_info(&self.crash_dump, &self.filepath, pid, &self.colors)
469 }
470
471 pub fn get_stack_info(&self, pid: &str) -> io::Result<Text> {
472 self.parser
473 .get_stack_info(&self.crash_dump, &self.filepath, pid, &self.colors)
474 }
475
476 pub fn get_message_queue_info(&self, pid: &str) -> io::Result<Text> {
477 self.parser
478 .get_message_queue_info(&self.crash_dump, &self.filepath, pid, &self.colors)
479 }
480}
481
482impl App<'_> {
484 pub fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
485 let titles = SelectedTab::iter().map(SelectedTab::title);
486 let highlight_style = (Color::default(), self.selected_tab.palette().c700);
487 let selected_tab_index = self.selected_tab as usize;
488 Tabs::new(titles)
489 .highlight_style(highlight_style)
490 .select(selected_tab_index)
491 .padding("", "")
492 .divider(" ")
493 .render(area, buf);
494 }
495
496 pub fn get_selected_pid(&self) -> String {
497 if self.selected_tab == SelectedTab::Process {
498 let process_table_state = self.table_states.get(&SelectedTab::Process).unwrap();
499 let selected_item = process_table_state.selected().unwrap_or(0);
500 self.tab_lists[&SelectedTab::Process][selected_item].clone()
501 } else {
502 String::new()
503 }
504 }
505}
506
507impl Widget for &mut App<'_> {
508 fn render(self, area: Rect, buf: &mut Buffer) {
509 use Constraint::{Length, Min};
510 let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
511 let [header_area, inner_area, footer_area] = vertical.areas(area);
512
513 let horizontal = Layout::horizontal([Min(0), Length(20)]);
514 let [tabs_area, title_area] = horizontal.areas(header_area);
515
516 render_title(title_area, buf);
517 self.render_tabs(tabs_area, buf);
518 match self.selected_tab {
519 SelectedTab::General => self.selected_tab.render_general(inner_area, buf, self),
520 SelectedTab::Process => self.selected_tab.render_process(inner_area, buf, self),
521 SelectedTab::ProcessGroup => self
522 .selected_tab
523 .render_process_group(inner_area, buf, self),
524 SelectedTab::Inspect => self.selected_tab.render_inspect(inner_area, buf, self),
525 }
526 let footer_text = "<-, -> to change tabs | Press q to quit | ? for help";
527 render_footer(footer_text, footer_area, buf);
532
533 if self.show_help {
534 let help_text = Text::from(vec![
536 Line::from(Span::styled("Keyboard Shortcuts", Style::default().bold())),
537 Line::from(""),
538 Line::from("General Navigation:"),
539 Line::from(" q / Esc : Quit Application"),
540 Line::from(" Ctrl+C : Quit Application"),
541 Line::from(" ? : Toggle this Help Popup"),
542 Line::from(" Left / Right : Change Tabs"),
543 Line::from(""),
544 Line::from("Process Group Tab ('Process Group Info'):"),
546 Line::from(" Up / k : Move Selection Up"),
547 Line::from(" Down / j : Move Selection Down"),
548 Line::from(" Shift+T : Sort by Total Memory Size"),
549 Line::from(" Shift+P : Sort by Pid"),
550 Line::from(" Shift+N : Sort by Name"),
551 Line::from(" Shift+C : Sort by Children Count"),
552 Line::from(" (Press same sort key again to toggle Asc/Desc direction)"),
553 Line::from(""),
554 Line::from("Process List Tab ('Process Info'):"),
556 Line::from(" Up / k : Move Selection Up"),
557 Line::from(" Down / j : Move Selection Down"),
558 Line::from(" S : View Process Stack (in lower pane)"),
559 Line::from(" H : View Process Heap (in lower pane)"),
560 Line::from(" M : View Process Message Queue (in lower pane)"),
561 Line::from(" I : Inspect Selected View (Stack/Heap/MsgQ) fullscreen"),
562 Line::from(" Shift+P : Sort by Pid"),
563 Line::from(" Shift+N : Sort by Name"),
564 Line::from(" Shift+Q : Sort by Msg Queue Len"),
565 Line::from(" Shift+M : Sort by Memory"),
566 Line::from(" Shift+T : Sort by TotalBinVHeap"),
567 Line::from(" Shift+B : Sort by BinVHeap"),
568 Line::from(" Shift+U : Sort by BinVHeap Unused"),
569 Line::from(" Shift+O : Sort by OldBinVHeap"),
570 Line::from(" Shift+V : Sort by OldBinVHeap Unused"),
571 Line::from(" (Press same sort key again to toggle Asc/Desc direction)"),
572 Line::from(""),
573 Line::from("Inspect View:"),
575 Line::from(" Up / k : Scroll Up"),
576 Line::from(" Down / j : Scroll Down"),
577 Line::from(" PgUp / b : Page Up"),
578 Line::from(" PgDown / f : Page Down"),
579 Line::from(" Home / g : Go to Top"),
580 Line::from(" End / G : Go to Bottom"),
581 Line::from(" I : Return to Process List"),
582 ]);
583
584 let block = Block::bordered()
585 .title(Line::from(vec![
586 Span::styled(
587 " Help ",
588 Style::default().fg(self.colors.header_text).bold(),
589 ),
590 Span::styled(
591 "[?/Esc/q to close]",
592 Style::default().fg(self.colors.default_text),
593 ),
594 ]))
595 .border_style(Style::default().fg(self.colors.border_color))
596 .style(Style::default().bg(self.colors.background_color));
597
598 let paragraph = Paragraph::new(help_text)
599 .block(block)
600 .wrap(Wrap { trim: true });
601
602 let area = centered_rect(70, 90, area);
603
604 Clear.render(area, buf);
605 paragraph.render(area, buf);
606 }
607 }
608}
609
610impl SelectedTab {
611 fn previous(self) -> Self {
613 let current_index: usize = self as usize;
614 let previous_index = current_index.saturating_sub(1);
615 Self::from_repr(previous_index).unwrap_or(self)
616 }
617
618 fn next(self) -> Self {
620 let current_index = self as usize;
621 let next_index = current_index.saturating_add(1);
622 Self::from_repr(next_index).unwrap_or(self)
623 }
624}
625
626impl SelectedTab {
627 fn title(self) -> Line<'static> {
629 format!(" {self} ")
630 .fg(tailwind::SLATE.c200)
631 .bg(self.palette().c900)
632 .into()
633 }
634
635 fn render_general(self, area: Rect, buf: &mut Buffer, app: &mut App) {
636 let preamble_text = app.crash_dump.preamble.format();
637 let process_count = app.index_map[&Tag::Proc].len();
638 let ets_count = app.index_map[&Tag::Ets].len();
639 let fn_count = app.index_map[&Tag::Fun].len();
640
641 let memory_info_text = app.crash_dump.memory.format();
642
643 let preamble_lines: Vec<Line> = preamble_text
645 .lines()
646 .map(|line| {
647 Line::from(Span::styled(
648 line,
649 Style::default().fg(app.colors.default_text),
650 ))
651 })
652 .collect();
653
654 let memory_information_lines: Vec<Line> = memory_info_text
656 .lines()
657 .map(|line| {
658 Line::from(Span::styled(
659 line,
660 Style::default().fg(app.colors.default_text),
661 ))
662 })
663 .collect();
664
665 let memory_information_header = Line::from(vec![
667 Span::styled(
668 "Memory Information:",
669 Style::default().fg(app.colors.info_preamble),
670 ),
671 Span::raw("\n"),
672 ]);
673
674 let process_count = Line::from(vec![
675 Span::styled("Process Count: ", Style::default().fg(app.colors.info_preamble)),
676 Span::styled(
677 process_count.to_string(),
678 Style::default().fg(app.colors.default_text),
679 ),
680 ]);
681
682 let ets_count = Line::from(vec![
683 Span::styled("ETS Tables: ", Style::default().fg(app.colors.info_preamble)),
684 Span::styled(
685 ets_count.to_string(),
686 Style::default().fg(app.colors.default_text),
687 ),
688 ]);
689
690 let fn_count = Line::from(vec![
691 Span::styled("Funs: ", Style::default().fg(app.colors.info_preamble)),
692 Span::styled(
693 fn_count.to_string(),
694 Style::default().fg(app.colors.default_text),
695 ),
696 ]);
697
698 let mut general_info_text = Text::from(preamble_lines);
700 general_info_text.extend(vec![memory_information_header]);
701 general_info_text.extend(memory_information_lines);
702 general_info_text.extend(process_count);
703 general_info_text.extend(ets_count);
704 general_info_text.extend(fn_count);
705
706 let paragraph = Paragraph::new(general_info_text)
707 .block(Block::bordered().title("General Information"))
708 .style(Style::default().fg(app.colors.default_text))
709 .alignment(Alignment::Left);
710
711 Widget::render(¶graph, area, buf);
712 }
713
714 fn render_process(self, area: Rect, buf: &mut Buffer, app: &mut App) {
732 let outer_layout = Layout::default()
733 .direction(Direction::Vertical)
734 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
735 .split(area);
736
737 let inner_layout = Layout::default()
739 .direction(Direction::Horizontal)
740 .constraints(vec![Constraint::Percentage(25), Constraint::Percentage(75)])
741 .split(outer_layout[1]);
742
743 let selected_item;
744 {
745 let process_table_state = app.table_states.get_mut(&SelectedTab::Process).unwrap();
746 selected_item = process_table_state.selected().unwrap_or(0);
747 StatefulWidget::render(
748 &app.process_view_table,
749 outer_layout[0],
750 buf,
751 process_table_state,
752 );
753 }
754
755 let selected_pid = &app.tab_lists[&SelectedTab::Process][selected_item];
756 let selected_process_result = app.crash_dump.processes.get(selected_pid);
757
758 let active_proc_info: types::ProcInfo;
759 let process_info_text: Text;
760 match selected_process_result {
761 Some(process_ref) => {
762 let text = match *process_ref.value() {
763 InfoOrIndex::Info(ref proc_info) => {
764 let proc_info: &types::ProcInfo = proc_info;
765 active_proc_info = proc_info.clone();
766 active_proc_info.format_as_ratatui_text(&app.colors)
767 }
768 InfoOrIndex::Index(_) => {
769 Text::raw(format!("Index for pid: {:?}", selected_pid).to_string())
770 }
771 };
772 process_info_text = text;
773 }
774 None => {
775 process_info_text =
776 Text::raw(format!("Process not found: {:?}", selected_pid).to_string());
777 }
778 };
779
780 let (inspect_info_title, inspect_info_text) = match app.process_view_state {
781 ProcessViewState::Stack => {
782 app.inspecting_pid = selected_pid.clone();
783 ("Decoded Stack", app.get_stack_info(selected_pid).unwrap())
784 }
785 ProcessViewState::Heap => {
786 app.inspecting_pid = selected_pid.clone();
787
788 ("Decoded Heap", app.get_heap_info(selected_pid).unwrap())
789 }
790 ProcessViewState::MessageQueue => {
791 app.inspecting_pid = selected_pid.clone();
792 (
793 "Decoded Message Queue",
794 app.get_message_queue_info(selected_pid).unwrap(),
795 )
796 }
797 };
798
799 let detail_block = Paragraph::new(process_info_text)
802 .block(Block::bordered().title("Process Details"))
803 .style(Style::default().fg(app.colors.default_text))
804 .wrap(Wrap { trim: false })
805 .alignment(Alignment::Left);
806
807 let proc_heap = Paragraph::new(inspect_info_text)
808 .block(Block::bordered().title(inspect_info_title))
809 .style(Style::default().fg(app.colors.default_text))
810 .alignment(Alignment::Left);
811
812 Widget::render(&detail_block, inner_layout[0], buf);
813 Widget::render(&proc_heap, inner_layout[1], buf);
814 }
815
816 fn render_process_group(self, area: Rect, buf: &mut Buffer, app: &mut App) {
817 let outer_layout = Layout::default()
818 .direction(Direction::Horizontal)
819 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
820 .split(area);
821
822 let inner_layout = Layout::default()
824 .direction(Direction::Vertical)
825 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
826 .split(outer_layout[1]);
827
828 let group_table_state = app
829 .table_states
830 .get_mut(&SelectedTab::ProcessGroup)
831 .unwrap();
832
833 let selected_item = group_table_state.selected().unwrap_or(0);
834 let selected_pid = &app.tab_lists[&SelectedTab::ProcessGroup][selected_item];
835 let selected_process_result = app.crash_dump.processes.get(selected_pid);
836
837 let active_proc_info: types::ProcInfo;
838 let process_info_text: Text;
839 match selected_process_result {
840 Some(process_ref) => {
841 let text = match *process_ref.value() {
842 InfoOrIndex::Info(ref proc_info) => {
843 let proc_info: &types::ProcInfo = proc_info;
844 active_proc_info = proc_info.clone();
845 active_proc_info.format_as_ratatui_text(&app.colors)
846 }
847 InfoOrIndex::Index(_) => {
848 Text::raw(format!("Index for pid: {:?}", selected_pid).to_string())
849 }
850 };
851 process_info_text = text;
852 }
853 None => {
854 process_info_text =
855 Text::raw(format!("Process not found: {:?}", selected_pid).to_string());
856 }
857 };
858
859 let children: Vec<Row> = match app.ancestor_map.get(selected_pid) {
860 Some(child_pids) => {
861 child_pids
862 .iter() .map(|child_pid| {
864 match app.crash_dump.processes.get(child_pid) {
865 Some(child_info_ref) => {
866 match *child_info_ref.value() {
867 InfoOrIndex::Info(ref proc_info) => {
869 Row::new(proc_info.summary_ref_array())
870 }
871 InfoOrIndex::Index(_) => {
872 Row::new(vec![format!("{:?}", child_pid)])
873 } }
875 }
876 None => {
877 Row::new(vec![format!("Info not found: {:?}", child_pid)])
879 }
880 }
881 })
882 .collect()
883 }
884 None => vec![Row::new(vec!["No data".to_string()])],
885 };
886
887 let children_block = Table::new(
889 children,
890 [
891 Constraint::Length(15),
892 Constraint::Length(60),
893 Constraint::Length(10),
894 Constraint::Length(20),
895 Constraint::Length(25),
896 ],
897 )
898 .header(
899 ["Pid", "Name", "Memory", "Reductions", "MsgQ Length"]
900 .iter()
901 .map(|&h| Cell::from(h))
902 .collect::<Row>()
903 .style(
904 Style::default()
905 .fg(app.colors.default_text)
906 .bg(app.colors.header_background),
907 ),
908 )
909 .highlight_spacing(HighlightSpacing::Always)
910 .block(Block::bordered().title("Group Children"));
911
912 let detail_block = Paragraph::new(process_info_text)
913 .block(Block::bordered().title("Ancestor Details"))
914 .style(Style::default().fg(app.colors.default_text))
915 .wrap(Wrap { trim: false })
916 .alignment(Alignment::Left);
917
918 Widget::render(&children_block, inner_layout[0], buf);
919 Widget::render(&detail_block, inner_layout[1], buf);
920 StatefulWidget::render(
921 &app.process_group_table,
922 outer_layout[0],
923 buf,
924 group_table_state,
925 );
926 }
927
928 fn render_inspect(self, area: Rect, buf: &mut Buffer, app: &mut App) {
929 let width = if buf.area.height < 70 {
930 buf.area.width - 1
931 } else {
932 buf.area.width
933 };
934 let mut scroll_view = ScrollView::new(Size::new(width, 70));
935
936 let inspect_info_text;
937 let inspect_info_title;
938 {
939 let (t1, t2) = match app.process_view_state {
940 ProcessViewState::Stack => (
941 "Decoded Stack",
942 app.get_stack_info(&app.inspecting_pid).unwrap(),
943 ),
944 ProcessViewState::Heap => (
945 "Decoded Heap",
946 app.get_heap_info(&app.inspecting_pid).unwrap(),
947 ),
948 ProcessViewState::MessageQueue => (
949 "Decoded Message Queue",
950 app.get_message_queue_info(&app.inspecting_pid).unwrap(),
951 ),
952 };
953 inspect_info_title = t1;
954 inspect_info_text = t2.clone();
955 }
956
957 let proc_info = Paragraph::new(inspect_info_text)
958 .block(Block::bordered().title(inspect_info_title))
959 .style(Style::default().fg(app.colors.default_text))
960 .wrap(Wrap { trim: false })
961 .alignment(Alignment::Left);
962
963 proc_info.render(area, &mut scroll_view.buf_mut());
964 scroll_view.render(area, buf, &mut app.inspect_scroll_state);
965 }
966
967 const fn palette(self) -> tailwind::Palette {
968 match self {
969 Self::General => tailwind::BLUE,
970 Self::Process => tailwind::EMERALD,
971 Self::ProcessGroup => tailwind::INDIGO,
972 Self::Inspect => tailwind::PURPLE,
973 }
974 }
975}
976
977fn render_title(area: Rect, buf: &mut Buffer) {
978 "ERL Crash Dump".render(area, buf);
979}
980
981fn render_footer(footer_text: &str, area: Rect, buf: &mut Buffer) {
982 Line::raw(footer_text).centered().render(area, buf);
983}
984
985fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
986 let popup_layout = Layout::vertical([
987 Constraint::Percentage((100 - percent_y) / 2),
988 Constraint::Percentage(percent_y),
989 Constraint::Percentage((100 - percent_y) / 2),
990 ])
991 .split(r);
992
993 Layout::horizontal([
994 Constraint::Percentage((100 - percent_x) / 2),
995 Constraint::Percentage(percent_x),
996 Constraint::Percentage((100 - percent_x) / 2),
997 ])
998 .split(popup_layout[1])[1]
999}