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