Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Adds tree view #223

Merged
merged 21 commits into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix line generation, basic version of filtering
  • Loading branch information
ClementTsang committed Sep 5, 2020
commit 86f921eaa2d1ca9e9fb8d372d806aca9b3f227b1
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"fract",
"gnueabihf",
"gotop",
"gotop's",
"gtop",
"haase",
"heim",
Expand Down
2 changes: 2 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ impl App {
.search_state
.is_enabled = false;
self.move_widget_selection(&WidgetDirection::Up);
self.is_force_redraw = true;
return;
}
}
Expand All @@ -215,6 +216,7 @@ impl App {
current_proc_state.columns.backup_prev_scroll_position;
current_proc_state.is_sort_open = false;
self.move_widget_selection(&WidgetDirection::Right);
self.is_force_redraw = true;
return;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/data_harvester/processes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ pub fn linux_get_processes_list(
pid_mapping: &mut HashMap<Pid, PrevProcDetails, RandomState>, use_current_cpu_total: bool,
time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
// TODO: [PROC THREADS] Add threads

if let Ok((cpu_usage, cpu_fraction)) = cpu_usage_calculation(prev_idle, prev_non_idle) {
let process_vector: Vec<ProcessHarvest> = std::fs::read_dir("/proc")?
.filter_map(|dir| {
Expand Down
1 change: 0 additions & 1 deletion src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ pub struct DisplayableData {
pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>,
pub single_process_data: Vec<ConvertedProcessData>, // Contains single process data
pub process_data: Vec<ConvertedProcessData>, // Not the final value, may be grouped or single
pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed
pub mem_label_percent: String,
pub swap_label_percent: String,
Expand Down
4 changes: 3 additions & 1 deletion src/canvas/canvas_colours.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct CanvasColours {
// Full, Medium, Low
pub battery_bar_styles: Vec<Style>,
pub invalid_query_style: Style,
pub disabled_text_style: Style,
}

impl Default for CanvasColours {
Expand Down Expand Up @@ -63,7 +64,8 @@ impl Default for CanvasColours {
Style::default().fg(Color::Green),
Style::default().fg(Color::Green),
],
invalid_query_style: tui::style::Style::default().fg(tui::style::Color::Red),
invalid_query_style: Style::default().fg(tui::style::Color::Red),
disabled_text_style: Style::default().fg(Color::DarkGray),
}
}
}
Expand Down
67 changes: 36 additions & 31 deletions src/canvas/widgets/process_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,38 +213,42 @@ impl ProcessTableWidget for Painter {

// FIXME: [PROC OPTIMIZE] This can definitely be optimized; string references work fine here!
let process_rows = sliced_vec.iter().map(|process| {
Row::Data(
vec![
if is_proc_widget_grouped {
process.group_pids.len().to_string()
} else {
process.pid.to_string()
},
if is_tree {
if let Some(prefix) = &process.process_description_prefix {
prefix.clone()
} else {
String::default()
}
} else if is_using_command {
process.command.clone()
} else {
process.name.clone()
},
format!("{:.1}%", process.cpu_percent_usage),
if mem_enabled {
format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
let data = vec![
if is_proc_widget_grouped {
process.group_pids.len().to_string()
} else {
process.pid.to_string()
},
if is_tree {
if let Some(prefix) = &process.process_description_prefix {
prefix.clone()
} else {
format!("{:.1}%", process.mem_percent_usage)
},
process.read_per_sec.clone(),
process.write_per_sec.clone(),
process.total_read.clone(),
process.total_write.clone(),
process.process_state.clone(),
]
.into_iter(),
)
String::default()
}
} else if is_using_command {
process.command.clone()
} else {
process.name.clone()
},
format!("{:.1}%", process.cpu_percent_usage),
if mem_enabled {
format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
} else {
format!("{:.1}%", process.mem_percent_usage)
},
process.read_per_sec.clone(),
process.write_per_sec.clone(),
process.total_read.clone(),
process.total_write.clone(),
process.process_state.clone(),
]
.into_iter();

if process.is_disabled_entry {
Row::StyledData(data, self.colours.disabled_text_style)
} else {
Row::Data(data)
}
});

let process_headers = proc_widget_state.columns.get_column_headers(
Expand Down Expand Up @@ -280,6 +284,7 @@ impl ProcessTableWidget for Painter {
let intrinsic_widths =
&(variable_intrinsic_results.0)[0..variable_intrinsic_results.1];

// TODO: gotop's "x out of y" thing is really nice to help keep track of the scroll position.
f.render_stateful_widget(
Table::new(process_headers.iter(), process_rows)
.block(process_block)
Expand Down
98 changes: 77 additions & 21 deletions src/data_conversion.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! This mainly concerns converting collected data into things that the canvas
//! can actually handle.

use std::collections::{HashMap, VecDeque};
use std::collections::{BTreeSet, HashMap, VecDeque};

use crate::Pid;
use crate::{
Expand Down Expand Up @@ -62,8 +62,10 @@ pub struct ConvertedProcessData {
pub tw_f64: f64,
pub process_state: String,
pub process_char: char,
// Prefix printed before the process when displayed.
/// Prefix printed before the process when displayed.
pub process_description_prefix: Option<String>,
/// Whether to mark this process entry as disabled (mostly for tree mode).
pub is_disabled_entry: bool,
}

#[derive(Clone, Default, Debug)]
Expand Down Expand Up @@ -453,30 +455,31 @@ pub fn convert_process_data(
process_state: process.process_state.to_owned(),
process_char: process.process_state_char,
process_description_prefix: None,
is_disabled_entry: false,
}
})
.collect::<Vec<_>>()
}

const BRANCH_ENDING: char = '';
const BRANCH_VERTICAL: char = '';
const BRANCH_SPLIT: char = '';
const BRANCH_HORIZONTAL: char = '';
const BRANCH_ENDING: char = '';
const BRANCH_VERTICAL: char = '';
const BRANCH_SPLIT: char = '';
const BRANCH_HORIZONTAL: char = '';

pub fn tree_process_data(
single_process_data: &[ConvertedProcessData], is_using_command: bool,
) -> Vec<ConvertedProcessData> {
// Let's first build up a (really terrible) parent -> child mapping...
// At the same time, let's make a mapping of PID -> process data!
let mut parent_child_mapping: HashMap<Pid, Vec<Pid>> = HashMap::default();
let mut parent_child_mapping: HashMap<Pid, BTreeSet<Pid>> = HashMap::default();
let mut pid_process_mapping: HashMap<Pid, &ConvertedProcessData> = HashMap::default();

single_process_data.iter().for_each(|process| {
if let Some(ppid) = process.ppid {
parent_child_mapping
.entry(ppid)
.or_insert_with(Vec::new)
.push(process.pid);
.or_insert_with(BTreeSet::new)
.insert(process.pid);
}

// There should be no collisions...
Expand All @@ -494,29 +497,78 @@ pub fn tree_process_data(
};
let mut explored_pids: Vec<Pid> = vec![];
let mut lines: Vec<String> = vec![];
let num_lines: usize = 1;

// We do pid 0 separately as it's a bit special in some cases.
if let Some(zero_pid) = parent_child_mapping.get(&0) {
pids_to_explore.extend(zero_pid);
// TODO: Windows implementation...
}

/// A post-order traversal to correctly prune entire branches that only contain children
/// that are disabled and themselves are also disabled ~~wait that sounds wrong~~.
///
/// Basically, go through the hashmap, and prune out all branches that are no longer relevant.
fn prune_disabled_pids(
current_pid: Pid, parent_child_mapping: &mut HashMap<Pid, BTreeSet<Pid>>,
pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>,
) -> bool {
// Let's explore all the children first, and make sure they (and their children)
// aren't all disabled...
let mut are_all_children_disabled = true;
if let Some(children) = parent_child_mapping.get(&current_pid) {
for child_pid in children.clone() {
let is_child_disabled =
prune_disabled_pids(child_pid, parent_child_mapping, pid_process_mapping);

if is_child_disabled {
if let Some(current_mapping) = parent_child_mapping.get_mut(&current_pid) {
current_mapping.remove(&child_pid);
}
} else if are_all_children_disabled {
are_all_children_disabled = false;
}
}
}

// Now consider the current pid and whether to prune...
// If the node itself is not disabled, then never prune. If it is, then check if all
// of its are disabled.
if let Some(process) = pid_process_mapping.get(&current_pid) {
if process.is_disabled_entry && are_all_children_disabled {
parent_child_mapping.remove(&current_pid);
return true;
}
}

false
}

/// A DFS traversal to correctly build the prefix lines (the pretty '├' and '─' lines) and
/// the correct order to the PID tree as a vector (DFS is the order htop uses so we're copying
/// that *wink*).
fn build_explored_pids(
current_pid: Pid, num_lines: usize, parent_child_mapping: &HashMap<Pid, Vec<Pid>>,
current_pid: Pid, parent_child_mapping: &HashMap<Pid, BTreeSet<Pid>>,
prev_drawn_lines: &str,
) -> (Vec<Pid>, Vec<String>) {
let mut explored_pids: Vec<Pid> = vec![current_pid];
let mut lines: Vec<String> = vec![];

if let Some(children) = parent_child_mapping.get(&current_pid) {
for (itx, child) in children.iter().rev().enumerate() {
let new_drawn_lines = if itx == children.len() - 1 {
format!("{} ", prev_drawn_lines)
} else {
format!("{}{} ", prev_drawn_lines, BRANCH_VERTICAL)
};

let (pid_res, branch_res) =
build_explored_pids(*child, num_lines + 1, parent_child_mapping);
build_explored_pids(*child, parent_child_mapping, new_drawn_lines.as_str());

if itx == children.len() - 1 {
lines.push(format!(
"{}{}",
format!("{} ", BRANCH_VERTICAL).repeat(num_lines.saturating_sub(1)),
if num_lines > 0 {
prev_drawn_lines,
if !new_drawn_lines.is_empty() {
format!("{}{} ", BRANCH_ENDING, BRANCH_HORIZONTAL)
} else {
String::default()
Expand All @@ -525,8 +577,8 @@ pub fn tree_process_data(
} else {
lines.push(format!(
"{}{}",
format!("{} ", BRANCH_VERTICAL).repeat(num_lines.saturating_sub(1)),
if num_lines > 0 {
prev_drawn_lines,
if !new_drawn_lines.is_empty() {
format!("{}{} ", BRANCH_SPLIT, BRANCH_HORIZONTAL)
} else {
String::default()
Expand All @@ -543,11 +595,14 @@ pub fn tree_process_data(
}

while let Some(current_pid) = pids_to_explore.pop_front() {
let (pid_res, branch_res) =
build_explored_pids(current_pid, num_lines, &parent_child_mapping);
lines.push(String::default());
lines.extend(branch_res);
explored_pids.extend(pid_res);
let is_disabled =
prune_disabled_pids(current_pid, &mut parent_child_mapping, &pid_process_mapping);
if !is_disabled {
let (pid_res, branch_res) = build_explored_pids(current_pid, &parent_child_mapping, "");
lines.push(String::default());
lines.extend(branch_res);
explored_pids.extend(pid_res);
}
}

// Now let's "rearrange" our current list of converted process data into the correct
Expand Down Expand Up @@ -654,6 +709,7 @@ pub fn group_process_data(
process_state: p.process_state, // TODO: What the heck
process_description_prefix: None,
process_char: char::default(), // TODO: What the heck
is_disabled_entry: false,
}
})
.collect::<Vec<_>>()
Expand Down
Loading