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