diff --git a/tokio-console/src/input.rs b/tokio-console/src/input.rs index 47cba9b7b..ce4e3a320 100644 --- a/tokio-console/src/input.rs +++ b/tokio-console/src/input.rs @@ -66,6 +66,90 @@ pub(crate) fn is_esc(event: &Event) -> bool { ) } +#[derive(Debug, Clone)] +pub(crate) enum Event { + Key(KeyEvent), + Mouse(MouseEvent), +} + +#[derive(Debug, Clone)] +pub(crate) struct KeyEvent { + pub code: KeyCode, + pub modifiers: KeyModifiers, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum KeyCode { + Char(char), + Enter, + Esc, + Backspace, + Left, + Right, + Up, + Down, + Home, + End, + PageUp, + PageDown, + Tab, + BackTab, + Delete, + Insert, + F(u8), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct KeyModifiers { + pub shift: bool, + pub control: bool, + pub alt: bool, + pub super_: bool, +} + +impl Default for KeyModifiers { + fn default() -> Self { + Self { + shift: false, + control: false, + alt: false, + super_: false, + } + } +} + +pub(crate) fn poll(dur: Duration) -> std::io::Result<Option<Event>> { + if crossterm::event::poll(dur)? { + let event = crossterm::event::read()?; + Ok(Some(convert_event(event))) + } else { + Ok(None) + } +} + +fn convert_event(event: Event) -> Event { + match event { + Event::Key(key) => Event::Key(KeyEvent { + code: convert_key_code(key.code), + modifiers: KeyModifiers { + shift: key.modifiers.contains(KeyModifiers::shift), + control: key.modifiers.contains(KeyModifiers::control), + alt: key.modifiers.contains(KeyModifiers::alt), + super_: key.modifiers.contains(KeyModifiers::super_), + }, + }), + Event::Mouse(mouse) => Event::Mouse(mouse), + _ => Event::Key(KeyEvent { + code: KeyCode::Char(' '), + modifiers: KeyModifiers::default(), + }), + } +} + +fn convert_key_code(code: KeyCode) -> KeyCode { + code +} + #[cfg(test)] mod tests { use super::*; diff --git a/tokio-console/src/state/mod.rs b/tokio-console/src/state/mod.rs index 4693f8cce..116b070fa 100644 --- a/tokio-console/src/state/mod.rs +++ b/tokio-console/src/state/mod.rs @@ -16,9 +16,12 @@ use std::{ convert::{TryFrom, TryInto}, fmt, rc::Rc, + sync::atomic::{AtomicU64, Ordering}, time::{Duration, SystemTime}, + vec::Vec, }; use tasks::{Details, Task, TasksState}; +use tokio::sync::watch; pub mod async_ops; pub mod histogram; @@ -30,7 +33,9 @@ pub(crate) use self::store::Id; pub(crate) type DetailsRef = Rc<RefCell<Option<Details>>>; -#[derive(Default, Debug)] +const UPDATE_BUFFER_SIZE: usize = 1000; +const UPDATE_BATCH_INTERVAL: Duration = Duration::from_millis(16); // ~60fps + pub(crate) struct State { metas: HashMap<u64, Metadata>, last_updated_at: Option<SystemTime>, @@ -41,6 +46,10 @@ pub(crate) struct State { current_task_details: DetailsRef, retain_for: Option<Duration>, strings: intern::Strings, + last_update: watch::Sender<Option<SystemTime>>, + update_buffer: Vec<UpdateEvent>, + last_batch_time: SystemTime, + update_counter: AtomicU64, } pub(crate) enum Visibility { @@ -100,7 +109,70 @@ pub(crate) struct Attribute { unit: Option<String>, } +#[derive(Debug)] +pub(crate) enum UpdateEvent { + TaskUpdate(Task), + ResourceUpdate(Resource), + AsyncOpUpdate(AsyncOp), +} + impl State { + pub(crate) fn new() -> (Self, watch::Receiver<Option<SystemTime>>) { + let (tx, rx) = watch::channel(None); + ( + Self { + metas: HashMap::new(), + last_updated_at: None, + temporality: Temporality::Live, + tasks_state: TasksState::default(), + resources_state: ResourcesState::default(), + async_ops_state: AsyncOpsState::default(), + current_task_details: Rc::new(RefCell::new(None)), + retain_for: None, + strings: intern::Strings::new(), + last_update: tx, + update_buffer: Vec::with_capacity(UPDATE_BUFFER_SIZE), + last_batch_time: SystemTime::now(), + update_counter: AtomicU64::new(0), + }, + rx, + ) + } + + pub(crate) fn buffer_update(&mut self, event: UpdateEvent) { + self.update_buffer.push(event); + self.update_counter.fetch_add(1, Ordering::SeqCst); + + let now = SystemTime::now(); + if now.duration_since(self.last_batch_time).unwrap_or(Duration::ZERO) >= UPDATE_BATCH_INTERVAL + || self.update_buffer.len() >= UPDATE_BUFFER_SIZE + { + self.flush_updates(); + } + } + + pub(crate) fn flush_updates(&mut self) { + if self.update_buffer.is_empty() { + return; + } + + for event in self.update_buffer.drain(..) { + match event { + UpdateEvent::TaskUpdate(task) => self.tasks_state.update_task(task), + UpdateEvent::ResourceUpdate(resource) => self.resources_state.update_resource(resource), + UpdateEvent::AsyncOpUpdate(async_op) => self.async_ops_state.update_async_op(async_op), + } + } + + let now = SystemTime::now(); + self.last_batch_time = now; + let _ = self.last_update.send(Some(now)); + } + + pub(crate) fn last_updated_at(&self) -> Option<SystemTime> { + *self.last_update.borrow() + } + pub(crate) fn with_retain_for(mut self, retain_for: Option<Duration>) -> Self { self.retain_for = retain_for; self @@ -114,10 +186,6 @@ impl State { self } - pub(crate) fn last_updated_at(&self) -> Option<SystemTime> { - self.last_updated_at - } - pub(crate) fn update( &mut self, styles: &view::Styles, diff --git a/tokio-console/src/state/tasks.rs b/tokio-console/src/state/tasks.rs index 692b65bcf..1e3b9f859 100644 --- a/tokio-console/src/state/tasks.rs +++ b/tokio-console/src/state/tasks.rs @@ -50,6 +50,7 @@ pub(crate) enum SortBy { Polls = 8, Target = 9, Location = 10, + LastUpdate = 11, } #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] @@ -621,6 +622,8 @@ impl SortBy { } Self::Location => tasks .sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().location.clone())), + Self::LastUpdate => tasks + .sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().last_update)), } } } @@ -646,6 +649,7 @@ impl TryFrom<usize> for SortBy { idx if idx == Self::Polls as usize => Ok(Self::Polls), idx if idx == Self::Target as usize => Ok(Self::Target), idx if idx == Self::Location as usize => Ok(Self::Location), + idx if idx == Self::LastUpdate as usize => Ok(Self::LastUpdate), _ => Err(()), } } diff --git a/tokio-console/src/view/mod.rs b/tokio-console/src/view/mod.rs index e8fff249e..91c746bf4 100644 --- a/tokio-console/src/view/mod.rs +++ b/tokio-console/src/view/mod.rs @@ -116,69 +116,67 @@ impl View { return update_kind; } - if matches!(event, key!(Char('t'))) { - self.state = TasksList; - return update_kind; - } - - if matches!(event, key!(Char('r'))) { - self.state = ResourcesList; - return update_kind; - } - - match self.state { - TasksList => { - // The enter key changes views, so handle here since we can - // mutate the currently selected view. - match event { - key!(Enter) => { - if let Some(task) = self.tasks_list.selected_item() { - update_kind = UpdateKind::SelectTask(task.borrow().span_id()); - self.state = TaskInstance(self::task::TaskView::new( - task, - state.task_details_ref(), - )); + match event { + input::Event::Mouse(mouse_event) => { + match mouse_event.kind { + MouseEventKind::Down(_) => { + match self.state { + TasksList => { + if self.tasks_list.handle_resize(mouse_event.column, &area) { + return update_kind; + } + } + ResourcesList => { + if self.resources_list.handle_resize(mouse_event.column, &area) { + return update_kind; + } + } + _ => {} } } - _ => { - // otherwise pass on to view - self.tasks_list.update_input(event); - } - } - } - ResourcesList => { - match event { - key!(Enter) => { - if let Some(res) = self.resources_list.selected_item() { - update_kind = UpdateKind::SelectResource(res.borrow().span_id()); - self.state = ResourceInstance(self::resource::ResourceView::new(res)); + MouseEventKind::Up(_) => { + match self.state { + TasksList => self.tasks_list.end_resize(), + ResourcesList => self.resources_list.end_resize(), + _ => {} } } - _ => { - // otherwise pass on to view - self.resources_list.update_input(event); + MouseEventKind::Moved => { + match self.state { + TasksList => { + if self.tasks_list.handle_resize(mouse_event.column, &area) { + return update_kind; + } + } + ResourcesList => { + if self.resources_list.handle_resize(mouse_event.column, &area) { + return update_kind; + } + } + _ => {} + } } + _ => {} } } - ResourceInstance(ref mut view) => { - // The escape key changes views, so handle here since we can - // mutate the currently selected view. - match event { - key!(Esc) => { - self.state = ResourcesList; - update_kind = UpdateKind::Other; - } - key!(Enter) => { - if let Some(op) = view.async_ops_table.selected_item() { - if let Some(task_id) = op.borrow().task_id() { - let task = self - .tasks_list - .sorted_items - .iter() - .filter_map(|i| i.upgrade()) - .find(|t| task_id == t.borrow().id()); + input::Event::Key(key_event) => { + if matches!(key_event, key!(Char('t'))) { + self.state = TasksList; + return update_kind; + } + + if matches!(key_event, key!(Char('r'))) { + self.state = ResourcesList; + return update_kind; + } - if let Some(task) = task { + match self.state { + TasksList => { + // The enter key changes views, so handle here since we can + // mutate the currently selected view. + match key_event { + key!(Enter) => { + if let Some(task) = self.tasks_list.selected_item() { update_kind = UpdateKind::SelectTask(task.borrow().span_id()); self.state = TaskInstance(self::task::TaskView::new( task, @@ -186,28 +184,77 @@ impl View { )); } } + _ => { + // otherwise pass on to view + self.tasks_list.update_input(key_event); + } } } - _ => { - // otherwise pass on to view - view.update_input(event); + ResourcesList => { + match key_event { + key!(Enter) => { + if let Some(res) = self.resources_list.selected_item() { + update_kind = UpdateKind::SelectResource(res.borrow().span_id()); + self.state = ResourceInstance(self::resource::ResourceView::new(res)); + } + } + _ => { + // otherwise pass on to view + self.resources_list.update_input(key_event); + } + } } - } - } - TaskInstance(ref mut view) => { - // The escape key changes views, so handle here since we can - // mutate the currently selected view. - match event { - key!(Esc) => { - self.state = TasksList; - update_kind = UpdateKind::ExitTaskView; + ResourceInstance(ref mut view) => { + // The escape key changes views, so handle here since we can + // mutate the currently selected view. + match key_event { + key!(Esc) => { + self.state = ResourcesList; + update_kind = UpdateKind::Other; + } + key!(Enter) => { + if let Some(op) = view.async_ops_table.selected_item() { + if let Some(task_id) = op.borrow().task_id() { + let task = self + .tasks_list + .sorted_items + .iter() + .filter_map(|i| i.upgrade()) + .find(|t| task_id == t.borrow().id()); + + if let Some(task) = task { + update_kind = UpdateKind::SelectTask(task.borrow().span_id()); + self.state = TaskInstance(self::task::TaskView::new( + task, + state.task_details_ref(), + )); + } + } + } + } + _ => { + // otherwise pass on to view + view.update_input(key_event); + } + } } - _ => { - // otherwise pass on to view - view.update_input(event); + TaskInstance(ref mut view) => { + // The escape key changes views, so handle here since we can + // mutate the currently selected view. + match key_event { + key!(Esc) => { + self.state = TasksList; + update_kind = UpdateKind::ExitTaskView; + } + _ => { + // otherwise pass on to view + view.update_input(key_event); + } + } } } } + _ => {} } update_kind } diff --git a/tokio-console/src/view/table.rs b/tokio-console/src/view/table.rs index c3d03959b..23f04345f 100644 --- a/tokio-console/src/view/table.rs +++ b/tokio-console/src/view/table.rs @@ -38,16 +38,92 @@ pub(crate) trait SortBy { fn as_column(&self) -> usize; } +pub(crate) struct ColumnResizeState { + pub resizing: bool, + pub column_index: usize, + pub initial_width: u16, + pub initial_x: u16, +} + +impl Default for ColumnResizeState { + fn default() -> Self { + Self { + resizing: false, + column_index: 0, + initial_width: 0, + initial_x: 0, + } + } +} + pub(crate) struct TableListState<T: TableList<N>, const N: usize> { pub(crate) sorted_items: Vec<Weak<RefCell<T::Row>>>, pub(crate) sort_by: T::Sort, pub(crate) selected_column: usize, pub(crate) sort_descending: bool, pub(crate) table_state: TableState, - + pub(crate) resize_state: ColumnResizeState, + pub(crate) column_widths: Vec<u16>, last_key_event: Option<input::KeyEvent>, } +impl<T, const N: usize> Default for TableListState<T, N> +where + T: TableList<N>, + T::Sort: Default, +{ + fn default() -> Self { + let sort_by = T::Sort::default(); + let selected_column = sort_by.as_column(); + Self { + sorted_items: Default::default(), + sort_by, + table_state: Default::default(), + selected_column, + sort_descending: false, + last_key_event: None, + resize_state: Default::default(), + column_widths: vec![0; N], + } + } +} + +impl<T, const N: usize> TableListState<T, N> +where + T: TableList<N>, +{ + pub(crate) fn handle_resize(&mut self, x: u16, area: &layout::Rect) -> bool { + if self.resize_state.resizing { + let delta = x.saturating_sub(self.resize_state.initial_x) as i32; + let new_width = (self.resize_state.initial_width as i32 + delta).max(5) as u16; + self.column_widths[self.resize_state.column_index] = new_width; + true + } else { + // Check if mouse is over column border + let mut current_x = area.x; + for (idx, &width) in self.column_widths.iter().enumerate() { + if x == current_x + width { + self.resize_state.resizing = true; + self.resize_state.column_index = idx; + self.resize_state.initial_width = width; + self.resize_state.initial_x = x; + return true; + } + current_x += width; + } + false + } + } + + pub(crate) fn end_resize(&mut self) { + self.resize_state.resizing = false; + } + + pub(crate) fn get_column_width(&self, idx: usize) -> u16 { + self.column_widths.get(idx).copied().unwrap_or(10) + } +} + impl<T: TableList<N>, const N: usize> TableListState<T, N> { pub(in crate::view) fn len(&self) -> usize { self.sorted_items.len() @@ -184,25 +260,6 @@ impl<T: TableList<N>, const N: usize> TableListState<T, N> { } } -impl<T, const N: usize> Default for TableListState<T, N> -where - T: TableList<N>, - T::Sort: Default, -{ - fn default() -> Self { - let sort_by = T::Sort::default(); - let selected_column = sort_by.as_column(); - Self { - sorted_items: Default::default(), - sort_by, - table_state: Default::default(), - selected_column, - sort_descending: false, - last_key_event: None, - } - } -} - impl<T, const N: usize> HelpText for TableListState<T, N> where T: TableList<N>, diff --git a/tokio-console/src/view/tasks.rs b/tokio-console/src/view/tasks.rs index 15e8f252c..ce1354f3b 100644 --- a/tokio-console/src/view/tasks.rs +++ b/tokio-console/src/view/tasks.rs @@ -20,17 +20,17 @@ use ratatui::{ #[derive(Debug, Default)] pub(crate) struct TasksTable {} -impl TableList<12> for TasksTable { +impl TableList<13> for TasksTable { type Row = Task; type Sort = SortBy; type Context = (); - const HEADER: &'static [&'static str; 12] = &[ + const HEADER: &'static [&'static str; 13] = &[ "Warn", "ID", "State", "Name", "Total", "Busy", "Sched", "Idle", "Polls", "Kind", - "Location", "Fields", + "Location", "Fields", "Last Update", ]; - const WIDTHS: &'static [usize; 12] = &[ + const WIDTHS: &'static [usize; 13] = &[ Self::HEADER[0].len() + 1, Self::HEADER[1].len() + 1, Self::HEADER[2].len() + 1, @@ -43,10 +43,11 @@ impl TableList<12> for TasksTable { Self::HEADER[9].len() + 1, Self::HEADER[10].len() + 1, Self::HEADER[11].len() + 1, + Self::HEADER[12].len() + 1, ]; fn render( - table_list_state: &mut TableListState<Self, 12>, + table_list_state: &mut TableListState<Self, 13>, styles: &view::Styles, frame: &mut ratatui::terminal::Frame, area: layout::Rect, @@ -260,6 +261,7 @@ impl TableList<12> for TasksTable { kind_width.constraint(), location_width.constraint(), fields_width, + layout::Constraint::Length(15), // Width for Last Update column ]; let table = table