crashdump_parser/
app.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
36/// Application result type.
37pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
38
39/// Application.
40pub struct App<'a> {
41    /// header
42    pub header: String,
43    pub state: AppState,
44    pub selected_tab: SelectedTab,
45
46    /// parser
47    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    /// process information list
56    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 = "Index")]
93    // Index,
94    #[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    /// Constructs a new instance of [`App`].
130    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        // store the index
141        // let idx_str = parser::CDParser::format_index(&idx);
142        // ret.tab_lists.get_mut(&SelectedTab::Index).map(|val| {
143        //     *val = idx_str;
144        // });
145
146        
147        ret.index_map = parser.build_index().unwrap();
148        ret.crash_dump = parser.parse(&ret.index_map).unwrap();   
149
150        //println!("heap addrs: {:?}", ret.crash_dump.all_heap_addresses);
151        //println!("binaries: {:?}", ret.crash_dump.visited_binaries);
152
153        ret.ancestor_map = parser::CDParser::create_descendants_table(&ret.crash_dump.processes);
154        // for every ancestor:<children> mapping, we need to calculate the GroupInfo for each one if the pid exists
155        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 all_processes = &mut ret.crash_dump.processes;
159
160        // set the process list to be a tuple of [pid, name, heap_size, msgq_len]
161        // we need to be able to sort an array based on the msgqlength as well
162
163        //////////////// Individual Process View
164        // let mut sorted_keys = ret
165        //     .crash_dump
166        //     .processes
167        //     .iter()
168        //     .collect::<Vec<(&String, &InfoOrIndex<ProcInfo>)>>();
169        // sorted_keys.sort_by(|a, b| match (a.1, b.1) {
170        //     (InfoOrIndex::Info(proc_info_a), InfoOrIndex::Info(proc_info_b)) => {
171        //         proc_info_b.memory.cmp(&proc_info_a.memory)
172        //     }
173        //     _ => unreachable!(),
174        // });
175
176        // let sorted_key_list = sorted_keys
177        //     .into_iter()
178        //     .map(|(key, _)| key.clone())
179        //     .collect::<Vec<String>>();
180
181        // ret.tab_lists.get_mut(&SelectedTab::Process).map(|val| {
182        //     *val = sorted_key_list;
183        // });
184
185        // let process_rows: Vec<Row> = ret.tab_lists[&SelectedTab::Process]
186        //     .iter()
187        //     .map(|pid| match ret.crash_dump.processes.get(pid).unwrap() {
188        //         InfoOrIndex::Info(proc_info) => {
189        //             let item = proc_info.ref_array();
190        //             Row::new(item)
191        //         }
192        //         _ => {
193        //             unreachable!();
194        //         }
195        //     })
196        //     .collect();
197
198        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() // Use parallel iterator
209            .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() // Use parallel iterator
217            .map(|pid| {
218                match ret.crash_dump.processes.get(pid) {
219                    Some(process_ref) => {
220                        match *process_ref.value() {
221                            // Dereference the Ref to access the inner value
222                            InfoOrIndex::Info(ref proc_info) => {
223                                let item = proc_info.ref_array();
224                                Row::new(item)
225                            }
226                            _ => {
227                                // Handle the Index case if it's possible in this context
228                                Row::new(vec![format!("Unexpected Index for pid: {:?}", pid)])
229                                // Or handle differently
230                            }
231                        }
232                    }
233                    None => {
234                        // Handle the case where the PID is not found in the DashMap
235                        Row::new(vec![format!("Process not found: {:?}", pid)]) // Or handle differently
236                    }
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        ///////// Process Group Info
275
276        let mut sorted_keys: Vec<(&String, &GroupInfo)> = ret
277            .crash_dump
278            .group_info_map
279            .par_iter() // Use parallel iterator
280            .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() // Use parallel iterator
284            .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() // Use parallel iterator
293            .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::Index) {
327        //     if !ret.tab_lists[&SelectedTab::Index].is_empty() {
328        //         state.select(Some(0));
329        //     }
330        // }
331
332        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    /// Handles the tick event of the terminal.
354    pub fn tick(&self) {}
355
356    /// Set running to false to quit the application.
357    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
385// Separated because this is the UI code. We need this here in order to render stuff *within* App state
386impl 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::Index => self.selected_tab.render_index(inner_area, buf, self),
424            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    /// Get the previous tab, if there is no previous tab return the current tab.
440    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    /// Get the next tab, if there is no next tab return the current tab.
447    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    /// Return tab's name as a styled `Line`
456    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        // Split the preamble text into lines
472        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        // Split the memory information text into lines
478        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        // Add a header for memory information
484        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        // Combine all lines into a single Text object
505        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(&paragraph, area, buf);
518    }
519
520    // fn render_index(self, area: Rect, buf: &mut Buffer, app: &mut App) {
521    //     let index_list_state = app.list_states.get_mut(&SelectedTab::Index).unwrap();
522    //     let list_items: Vec<ListItem> = app.tab_lists[&SelectedTab::Index]
523    //         .iter()
524    //         .map(|i| ListItem::new::<&str>(i.as_ref()))
525    //         .collect();
526
527    //     let binding = SelectedTab::Index.to_string();
528    //     let list = List::new(list_items)
529    //         .block(Block::bordered().title(binding.as_str()))
530    //         .highlight_symbol(">>")
531    //         .repeat_highlight_symbol(true)
532    //         .highlight_style(Style::default().bg(Color::Blue));
533
534    //     StatefulWidget::render(list, area, buf, index_list_state);
535    // }
536
537    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        // split the second side into the info side
544        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        //println!("heap info text: {}", heap_info_text);
605
606        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        // split the second side into the info side
628        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() // Use iter() here as we are just borrowing the child_pids
668                    .map(|child_pid| {
669                        match app.crash_dump.processes.get(child_pid) {
670                            Some(child_info_ref) => {
671                                match *child_info_ref.value() {
672                                    // Dereference the Ref
673                                    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                                    } // Format the pid
679                                }
680                            }
681                            None => {
682                                // Handle the case where child_pid is not found in processes
683                                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        // needs Pid, Name, Reductions, Memory, MsgQ Length,
693        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::Index => tailwind::TEAL,
770            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}