Skip to content

Commit

Permalink
Merge pull request #22 from tyrone-wu/enh/filter-name-type
Browse files Browse the repository at this point in the history
Added filters for Name and Type
  • Loading branch information
jfernandez authored Mar 18, 2024
2 parents 37ac873 + 946756c commit 4de0e3a
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 44 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ anyhow = "1.0.80"
ratatui = { version = "0.26.1", default-features = false, features = ['crossterm'] }
nix = { version = "0.28.0", features = ["user"] }
circular-buffer = "0.1.6"
procfs = "0.16.0"
procfs = "0.16.0"
tui-input = "0.8.0"
55 changes: 45 additions & 10 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ use std::{
use circular_buffer::CircularBuffer;
use libbpf_rs::query::ProgInfoIter;
use ratatui::widgets::TableState;
use tui_input::Input;

use crate::bpf_program::BpfProgram;

pub struct App {
pub mode: Mode,
pub state: Arc<Mutex<TableState>>,
pub items: Arc<Mutex<Vec<BpfProgram>>>,
pub data_buf: Arc<Mutex<CircularBuffer<20, PeriodMeasure>>>,
pub show_graphs: bool,
pub max_cpu: f64,
pub max_eps: i64,
pub max_runtime: u64,
pub filter_input: Arc<Mutex<Input>>,
}

pub struct PeriodMeasure {
Expand All @@ -44,23 +46,32 @@ pub struct PeriodMeasure {
pub average_runtime_ns: u64,
}

#[derive(Debug, PartialEq)]
pub enum Mode {
Table,
Graph,
Filter,
}

impl App {
pub fn new() -> App {
App {
mode: Mode::Table,
state: Arc::new(Mutex::new(TableState::default())),
items: Arc::new(Mutex::new(vec![])),
data_buf: Arc::new(Mutex::new(CircularBuffer::<20, PeriodMeasure>::new())),
show_graphs: false,
max_cpu: 0.0,
max_eps: 0,
max_runtime: 0,
filter_input: Arc::new(Mutex::new(Input::default())),
}
}

pub fn start_background_thread(&self) {
let items = Arc::clone(&self.items);
let data_buf = Arc::clone(&self.data_buf);
let state = Arc::clone(&self.state);
let filter = Arc::clone(&self.filter_input);

thread::spawn(move || loop {
let loop_start = Instant::now();
Expand All @@ -71,6 +82,10 @@ impl App {
.map(|prog| (prog.id.clone(), prog))
.collect();

let filter = filter.lock().unwrap();
let filter_str = filter.value().to_lowercase();
drop(filter);

let iter = ProgInfoIter::default();
for prog in iter {
let instant = Instant::now();
Expand All @@ -84,9 +99,18 @@ impl App {
continue;
}

// Skip bpf program if it does not match filter
let bpf_type = prog.ty.to_string();
if !filter_str.is_empty()
&& !bpf_type.to_lowercase().contains(&filter_str)
&& !prog_name.to_lowercase().contains(&filter_str)
{
continue;
}

let mut bpf_program = BpfProgram {
id: prog.id.to_string(),
bpf_type: prog.ty.to_string(),
bpf_type,
name: prog_name,
prev_runtime_ns: 0,
run_time_ns: prog.run_time_ns,
Expand Down Expand Up @@ -144,7 +168,10 @@ impl App {
self.max_cpu = 0.0;
self.max_eps = 0;
self.max_runtime = 0;
self.show_graphs = !self.show_graphs;
self.mode = match &self.mode {
Mode::Table => Mode::Graph,
_ => Mode::Table,
}
}

pub fn selected_program(&self) -> Option<BpfProgram> {
Expand All @@ -153,6 +180,7 @@ impl App {

state.selected().map(|i| items[i].clone())
}

pub fn next_program(&mut self) {
let items = self.items.lock().unwrap();
if items.len() > 0 {
Expand Down Expand Up @@ -188,6 +216,13 @@ impl App {
state.select(Some(i));
}
}

pub fn toggle_filter(&mut self) {
self.mode = match &self.mode {
Mode::Table => Mode::Filter,
_ => Mode::Table,
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -316,12 +351,12 @@ mod tests {
fn test_toggle_graphs() {
let mut app = App::new();

// Initially, show_graphs is false
assert!(!app.show_graphs);
// Initially, UI should be in table mode
assert_eq!(app.mode, Mode::Table);

// After calling toggle_graphs, show_graphs should be true
// After calling toggle_graphs, UI should be in graph mode
app.toggle_graphs();
assert!(app.show_graphs);
assert_eq!(app.mode, Mode::Graph);

// Set max_cpu, max_eps, and max_runtime to non-zero values
app.max_cpu = 10.0;
Expand All @@ -333,9 +368,9 @@ mod tests {
average_runtime_ns: 100,
});

// After calling toggle_graphs, show_graphs should be false again
// After calling toggle_graphs, UI should be in table mode again
app.toggle_graphs();
assert!(!app.show_graphs);
assert_eq!(app.mode, Mode::Table);

// max_cpu, max_eps, and max_runtime should be reset to 0
assert_eq!(app.max_cpu, 0.0);
Expand Down
99 changes: 66 additions & 33 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::os::fd::{FromRawFd, OwnedFd};
use std::time::Duration;

use anyhow::{anyhow, Context, Result};
use app::App;
use app::{App, Mode};
use bpf_program::BpfProgram;
use crossterm::event::{self, poll, Event, KeyCode, KeyModifiers};
use crossterm::execute;
Expand All @@ -39,12 +39,15 @@ use ratatui::widgets::{
Table,
};
use ratatui::{symbols, Frame, Terminal};
use tui_input::backend::crossterm::EventHandler;

mod app;
mod bpf_program;

const TABLE_FOOTER: &str = "(q) quit | (↑,k) move up | (↓,j) move down | (↵) show graphs";
const TABLE_FOOTER: &str =
"(q) quit | (↑,k) move up | (↓,j) move down | (↵) show graphs | (f) filter";
const GRAPHS_FOOTER: &str = "(q) quit | (↵) show program list";
const FILTER_FOOTER: &str = "(↵,Esc) back";
const HEADER_COLS: [&str; 7] = [
"ID",
"Type",
Expand Down Expand Up @@ -140,27 +143,29 @@ fn run_draw_loop<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result
// wait up to 100ms for a keyboard event
if poll(Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Down | KeyCode::Char('j') => {
if !app.show_graphs {
app.next_program()
match app.mode {
Mode::Table => match key.code {
KeyCode::Down | KeyCode::Char('j') => app.next_program(),
KeyCode::Up | KeyCode::Char('k') => app.previous_program(),
KeyCode::Enter => app.toggle_graphs(),
KeyCode::Char('f') => app.toggle_filter(),
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
_ => {}
},
Mode::Graph => match key.code {
KeyCode::Enter | KeyCode::Esc => app.toggle_graphs(),
KeyCode::Char('q') => return Ok(()),
_ => {}
},
Mode::Filter => match key.code {
KeyCode::Enter | KeyCode::Esc => app.toggle_filter(),
_ => {
app.filter_input
.lock()
.unwrap()
.handle_event(&Event::Key(key));
}
}
KeyCode::Up | KeyCode::Char('k') => {
if !app.show_graphs {
app.previous_program()
}
}
KeyCode::Enter => app.toggle_graphs(),
KeyCode::Char('q') => return Ok(()),
KeyCode::Esc => {
if app.show_graphs {
app.toggle_graphs()
} else {
return Ok(());
}
}
_ => {}
},
}
if let (KeyModifiers::CONTROL, KeyCode::Char('c')) = (key.modifiers, key.code) {
return Ok(());
Expand All @@ -175,14 +180,13 @@ fn ui(f: &mut Frame, app: &mut App) {

// This can happen when the program exists while the user is viewing the graphs.
// In this case, we want to switch back to the table view.
if app.selected_program().is_none() && app.show_graphs {
app.show_graphs = false;
if app.selected_program().is_none() && app.mode == Mode::Graph {
app.mode = Mode::Table;
}

if app.show_graphs {
render_graphs(f, app, rects[0]);
} else {
render_table(f, app, rects[0]);
match app.mode {
Mode::Table | Mode::Filter => render_table(f, app, rects[0]),
Mode::Graph => render_graphs(f, app, rects[0]),
}
render_footer(f, app, rects[1]);
}
Expand Down Expand Up @@ -428,17 +432,46 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
}

fn render_footer(f: &mut Frame, app: &mut App, area: Rect) {
let footer_text = if app.show_graphs {
GRAPHS_FOOTER
} else {
TABLE_FOOTER
let footer_text = match app.mode {
Mode::Table => TABLE_FOOTER,
Mode::Graph => GRAPHS_FOOTER,
Mode::Filter => FILTER_FOOTER,
};
let info_footer = Paragraph::new(Line::from(footer_text)).centered().block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Double),
);
f.render_widget(info_footer, area);

match app.mode {
Mode::Table | Mode::Graph => {
f.render_widget(info_footer, area);
}
Mode::Filter => {
let filter_area = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref())
.split(area);

let filter_input = app.filter_input.lock().unwrap();
let filter_footer = Paragraph::new(filter_input.value()).block(
Block::default()
.padding(Padding::horizontal(1))
.borders(Borders::ALL)
.border_type(BorderType::Double)
.title(" Filter Name/Type "),
);

f.render_widget(filter_footer, filter_area[0]); // Left filter footer
f.render_widget(info_footer, filter_area[1]); // Right info footer

// Displays cursor when inputting
f.set_cursor(
filter_area[0].x + filter_input.visual_cursor() as u16 + 2,
filter_area[0].y + 1,
)
}
}
}

fn running_as_root() -> bool {
Expand Down

0 comments on commit 4de0e3a

Please sign in to comment.