diff options
author | ClementTsang <cjhtsang@uwaterloo.ca> | 2021-08-31 16:53:43 -0400 |
---|---|---|
committer | ClementTsang <cjhtsang@uwaterloo.ca> | 2021-09-05 19:09:11 -0400 |
commit | 204b4dc3510b70618741f82a1e2df6c17c6dadda (patch) | |
tree | 5c13259b11bee7e044f18bb7a1ca33831d1af651 /src/app | |
parent | b1889b09344fea9eef98e5c2b9ce3b269dc8234b (diff) |
refactor: add back grouping and command
Diffstat (limited to 'src/app')
-rw-r--r-- | src/app/data_farmer.rs | 27 | ||||
-rw-r--r-- | src/app/widgets/base/sort_text_table.rs | 28 | ||||
-rw-r--r-- | src/app/widgets/base/text_input.rs | 16 | ||||
-rw-r--r-- | src/app/widgets/base/text_table.rs | 16 | ||||
-rw-r--r-- | src/app/widgets/process.rs | 171 |
5 files changed, 225 insertions, 33 deletions
diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index ccf2688e..25d265ab 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -56,7 +56,8 @@ pub struct DataCollection { pub cpu_harvest: cpu::CpuHarvest, pub load_avg_harvest: cpu::LoadAvgHarvest, pub process_harvest: Vec<processes::ProcessHarvest>, - pub process_count_mapping: HashMap<String, Pid>, + pub process_name_pid_map: HashMap<String, Vec<Pid>>, + pub process_cmd_pid_map: HashMap<String, Vec<Pid>>, pub disk_harvest: Vec<disks::DiskHarvest>, pub io_harvest: disks::IoHarvest, pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>, @@ -77,7 +78,8 @@ impl Default for DataCollection { cpu_harvest: cpu::CpuHarvest::default(), load_avg_harvest: cpu::LoadAvgHarvest::default(), process_harvest: Vec::default(), - process_count_mapping: HashMap::default(), + process_name_pid_map: HashMap::default(), + process_cmd_pid_map: HashMap::default(), disk_harvest: Vec::default(), io_harvest: disks::IoHarvest::default(), io_labels_and_prev: Vec::default(), @@ -312,6 +314,27 @@ impl DataCollection { } fn eat_proc(&mut self, list_of_processes: Vec<processes::ProcessHarvest>) { + // TODO: Probably more efficient to do this in the data collection step, but it's fine for now. + self.process_name_pid_map.clear(); + self.process_cmd_pid_map.clear(); + list_of_processes.iter().for_each(|process_harvest| { + if let Some(entry) = self.process_name_pid_map.get_mut(&process_harvest.name) { + entry.push(process_harvest.pid); + } else { + self.process_name_pid_map + .insert(process_harvest.name.to_string(), vec![process_harvest.pid]); + } + + if let Some(entry) = self.process_cmd_pid_map.get_mut(&process_harvest.command) { + entry.push(process_harvest.pid); + } else { + self.process_cmd_pid_map.insert( + process_harvest.command.to_string(), + vec![process_harvest.pid], + ); + } + }); + self.process_harvest = list_of_processes; } diff --git a/src/app/widgets/base/sort_text_table.rs b/src/app/widgets/base/sort_text_table.rs index 2b59f948..ceb7eb09 100644 --- a/src/app/widgets/base/sort_text_table.rs +++ b/src/app/widgets/base/sort_text_table.rs @@ -301,8 +301,28 @@ where &self.table.columns } - pub fn set_column(&mut self, column: S, index: usize) { - self.table.set_column(index, column) + pub fn set_column(&mut self, mut column: S, index: usize) { + if let Some(old_column) = self.table.columns().get(index) { + column.set_sorting_status(old_column.sorting_status()); + } + self.table.set_column(index, column); + } + + pub fn add_column(&mut self, column: S, index: usize) { + self.table.add_column(index, column); + } + + pub fn remove_column(&mut self, index: usize, new_sort_index: Option<usize>) { + self.table.remove_column(index); + + // Reset the sort index either a supplied one or a new one if needed. + if index == self.sort_index { + if let Some(new_sort_index) = new_sort_index { + self.set_sort_index(new_sort_index); + } else { + self.set_sort_index(0); + } + } } pub fn set_sort_index(&mut self, new_index: usize) { @@ -341,6 +361,10 @@ where } } + pub fn invalidate_cached_columns(&mut self) { + self.table.invalidate_cached_columns(); + } + /// Draws a [`tui::widgets::Table`] on screen. /// /// Note if the number of columns don't match in the [`SortableTextTable`] and data, diff --git a/src/app/widgets/base/text_input.rs b/src/app/widgets/base/text_input.rs index a6119c79..f3480c0e 100644 --- a/src/app/widgets/base/text_input.rs +++ b/src/app/widgets/base/text_input.rs @@ -44,7 +44,7 @@ pub struct TextInput { } impl Default for TextInput { - fn default() -> Self { + fn default() -> Self { Self { text: Default::default(), bounds: Default::default(), @@ -100,15 +100,17 @@ impl TextInput { fn move_word_forward(&mut self) -> WidgetEventResult { let current_index = self.cursor.cur_cursor(); - for (index, _word) in self.text[current_index..].unicode_word_indices() { - if index > current_index { - self.cursor.set_cursor(index); - self.cursor_direction = CursorDirection::Right; - return WidgetEventResult::Redraw; + if current_index < self.text.len() { + for (index, _word) in self.text[current_index..].unicode_word_indices() { + if index > 0 { + self.cursor.set_cursor(index + current_index); + self.cursor_direction = CursorDirection::Right; + return WidgetEventResult::Redraw; + } } + self.cursor.set_cursor(self.text.len()); } - self.cursor.set_cursor(self.text.len()); WidgetEventResult::Redraw } diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index 44f6d488..03111566 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -195,6 +195,18 @@ where } } + pub fn add_column(&mut self, index: usize, column: C) { + if self.columns.len() >= index { + self.columns.insert(index, column); + } + } + + pub fn remove_column(&mut self, index: usize) { + if self.columns.len() > index { + self.columns.remove(index); + } + } + pub fn current_scroll_index(&self) -> usize { self.scrollable.current_index() } @@ -360,6 +372,10 @@ where } } + pub fn invalidate_cached_columns(&mut self) { + self.cached_column_widths = CachedColumnWidths::Uncached; + } + /// Draws a [`Table`] on screen corresponding to the [`TextTable`]. pub fn draw_tui_table<B: Backend>( &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef, diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs index d127d02d..00554244 100644 --- a/src/app/widgets/process.rs +++ b/src/app/widgets/process.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use float_ord::FloatOrd; -use itertools::Itertools; +use itertools::{Either, Itertools}; use unicode_segmentation::GraphemeCursor; use tui::{ @@ -928,6 +928,64 @@ impl ProcessManager { pub fn is_case_sensitive(&self) -> bool { self.search_modifiers.enable_case_sensitive } + + fn is_using_command(&self) -> bool { + matches!( + self.process_table.columns()[1].sort_type, + ProcessSortType::Command + ) + } + + fn toggle_command(&mut self) -> WidgetEventResult { + if self.is_using_command() { + self.process_table + .set_column(ProcessSortColumn::new(ProcessSortType::Name), 1); + } else { + self.process_table + .set_column(ProcessSortColumn::new(ProcessSortType::Command), 1); + } + + // Invalidate row cache. + self.process_table.invalidate_cached_columns(); + + WidgetEventResult::Signal(ReturnSignal::Update) + } + + fn is_grouped(&self) -> bool { + matches!( + self.process_table.columns()[0].sort_type, + ProcessSortType::Count + ) + } + + fn toggle_grouped(&mut self) -> WidgetEventResult { + if self.is_grouped() { + self.process_table + .set_column(ProcessSortColumn::new(ProcessSortType::Pid), 0); + + self.process_table + .add_column(ProcessSortColumn::new(ProcessSortType::State), 8); + #[cfg(target_family = "unix")] + { + self.process_table + .add_column(ProcessSortColumn::new(ProcessSortType::User), 8); + } + } else { + self.process_table + .set_column(ProcessSortColumn::new(ProcessSortType::Count), 0); + + #[cfg(target_family = "unix")] + { + self.process_table.remove_column(9, Some(2)); + } + self.process_table.remove_column(8, Some(2)); + } + + // Invalidate row cache. + self.process_table.invalidate_cached_columns(); + + WidgetEventResult::Signal(ReturnSignal::Update) + } } impl Component for ProcessManager { @@ -964,9 +1022,11 @@ impl Component for ProcessManager { match event.code { KeyCode::Tab => { // Handle grouping/ungrouping + return self.toggle_grouped(); } KeyCode::Char('P') => { // Show full command/process name + return self.toggle_command(); } KeyCode::Char('d') => { match self.dd_multi.input('d') { @@ -1009,6 +1069,7 @@ impl Component for ProcessManager { } else if let KeyModifiers::SHIFT = event.modifiers { if let KeyCode::Char('P') = event.code { // Show full command/process name + return self.toggle_command(); } } @@ -1229,29 +1290,74 @@ impl Widget for ProcessManager { } fn update_data(&mut self, data_collection: &DataCollection) { - let filtered_sorted_iterator = data_collection - .process_harvest - .iter() - .filter(|process| { - if let Some(Ok(query)) = &self.process_filter { - query.check( - process, - matches!( - self.process_table.columns()[1].sort_type, - ProcessSortType::Command - ), - ) + let mut id_pid_map: HashMap<String, ProcessHarvest>; + + let filtered_iter = data_collection.process_harvest.iter().filter(|process| { + if let Some(Ok(query)) = &self.process_filter { + query.check(process, self.is_using_command()) + } else { + true + } + }); + + let filtered_grouped_iter = if self.is_grouped() { + id_pid_map = HashMap::new(); + filtered_iter.for_each(|process_harvest| { + let id = if self.is_using_command() { + &process_harvest.command + } else { + &process_harvest.name + }; + + if let Some(grouped_process_harvest) = id_pid_map.get_mut(id) { + grouped_process_harvest.cpu_usage_percent += process_harvest.cpu_usage_percent; + grouped_process_harvest.mem_usage_bytes += process_harvest.mem_usage_bytes; + grouped_process_harvest.mem_usage_percent += process_harvest.mem_usage_percent; + grouped_process_harvest.read_bytes_per_sec += + process_harvest.read_bytes_per_sec; + grouped_process_harvest.write_bytes_per_sec += + process_harvest.write_bytes_per_sec; + grouped_process_harvest.total_read_bytes += process_harvest.total_read_bytes; + grouped_process_harvest.total_write_bytes += process_harvest.total_write_bytes; } else { - true + id_pid_map.insert(id.clone(), process_harvest.clone()); } - }) - .sorted_by( + }); + + Either::Left(id_pid_map.values()) + } else { + Either::Right(filtered_iter) + }; + + let filtered_sorted_iter = if let ProcessSortType::Count = + self.process_table.current_sorting_column().sort_type + { + let mut v = filtered_grouped_iter.collect::<Vec<_>>(); + v.sort_by_cached_key(|k| { + if self.is_using_command() { + data_collection + .process_cmd_pid_map + .get(&k.command) + .map(|v| v.len()) + .unwrap_or(0) + } else { + data_collection + .process_name_pid_map + .get(&k.name) + .map(|v| v.len()) + .unwrap_or(0) + } + }); + Either::Left(v.into_iter()) + } else { + Either::Right(filtered_grouped_iter.sorted_by( match self.process_table.current_sorting_column().sort_type { ProcessSortType::Pid => { |a: &&ProcessHarvest, b: &&ProcessHarvest| a.pid.cmp(&b.pid) } ProcessSortType::Count => { - todo!() + // This case should be impossible by the above check. + unreachable!() } ProcessSortType::Name => { |a: &&ProcessHarvest, b: &&ProcessHarvest| a.name.cmp(&b.name) @@ -1287,14 +1393,15 @@ impl Widget for ProcessManager { } #[cfg(not(target_family = "unix"))] { - |_a: &&ProcessHarvest, _b: &&ProcessHarvest| Ord::Eq + |_a: &&ProcessHarvest, _b: &&ProcessHarvest| std::cmp::Ordering::Equal } } ProcessSortType::State => |a: &&ProcessHarvest, b: &&ProcessHarvest| { a.process_state.cmp(&b.process_state) }, }, - ); + )) + }; self.display_data = if let SortStatus::SortDescending = self .process_table @@ -1302,9 +1409,9 @@ impl Widget for ProcessManager { .sortable_column .sorting_status() { - itertools::Either::Left(filtered_sorted_iterator.rev()) + Either::Left(filtered_sorted_iter.rev()) } else { - itertools::Either::Right(filtered_sorted_iterator) + Either::Right(filtered_sorted_iter) } .map(|process| { self.process_table @@ -1312,7 +1419,27 @@ impl Widget for ProcessManager { .iter() .map(|column| match &column.sort_type { ProcessSortType::Pid => (process.pid.to_string().into(), None, None), - ProcessSortType::Count => ("".into(), None, None), + ProcessSortType::Count => ( + if self.is_using_command() { + data_collection + .process_cmd_pid_map + .get(&process.command) + .map(|v| v.len()) + .unwrap_or(0) + .to_string() + .into() + } else { + data_collection + .process_name_pid_map + .get(&process.name) + .map(|v| v.len()) + .unwrap_or(0) + .to_string() + .into() + }, + None, + None, + ), ProcessSortType::Name => (process.name.clone().into(), None, None), ProcessSortType::Command => (process.command.clone().into(), None, None), ProcessSortType::Cpu => ( |