From 53625e67cbc64405111d63369712ba182236929f Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:41:26 +0000 Subject: [PATCH 1/2] fix: reduce render execution in the err loop --- src/ui/mod.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3b17ea7..812acaa 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -126,30 +126,35 @@ impl Ui { let mut seconds = 5; let colors = self.app_data.lock().config.app_colors; let keymap = self.app_data.lock().config.keymap.clone(); + let mut render = true; loop { if self.now.elapsed() >= std::time::Duration::from_secs(1) { seconds -= 1; self.now = Instant::now(); + render = true; if seconds < 1 { break; } } - if self - .terminal - .draw(|f| { - draw_blocks::error::draw( - colors, - &AppError::DockerConnect, - f, - &keymap, - Some(seconds), - ); - }) - .is_err() + if render + && self + .terminal + .draw(|f| { + draw_blocks::error::draw( + colors, + &AppError::DockerConnect, + f, + &keymap, + Some(seconds), + ); + }) + .is_err() { return Err(AppError::Terminal); } + render = false; + std::thread::sleep(POLL_RATE); } Ok(()) } From bfc295c50e982886ccaa5e60b57f10d3690b3f09 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:40:34 +0000 Subject: [PATCH 2/2] fix: Only re-draw the screen if data/layout has changed --- src/app_data/mod.rs | 58 +++++++++++++++++++++++++++++++++-- src/config/mod.rs | 12 ++++---- src/docker_data/mod.rs | 3 +- src/input_handler/mod.rs | 2 +- src/main.rs | 18 +++++++---- src/ui/draw_blocks/mod.rs | 5 ++-- src/ui/gui_state.rs | 47 ++++++++++++++++++++++------- src/ui/mod.rs | 63 ++++++++++++++++++++++++++------------- src/ui/redraw.rs | 25 ++++++++++++++++ 9 files changed, 184 insertions(+), 49 deletions(-) create mode 100644 src/ui/redraw.rs diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 01e8b4e..658e2e4 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -13,7 +13,7 @@ mod container_state; use crate::{ app_error::AppError, config::Config, - ui::{log_sanitizer, GuiState, Status}, + ui::{log_sanitizer, GuiState, Redraw, Status}, ENTRY_POINT, }; pub use container_state::*; @@ -122,7 +122,9 @@ pub struct AppData { error: Option, filter: Filter, hidden_containers: Vec, + redraw: Arc, sorted_by: Option<(Header, SortedOrder)>, + current_sorted_id: Vec, pub config: Config, } @@ -134,18 +136,22 @@ pub struct AppData { pub error: Option, pub filter: Filter, pub hidden_containers: Vec, + pub current_sorted_id: Vec, + pub redraw: Arc, pub sorted_by: Option<(Header, SortedOrder)>, } impl AppData { /// Generate a default app_state - pub fn default(config: Config) -> Self { + pub fn new(config: Config, redraw: &Arc) -> Self { Self { config, containers: StatefulList::new(vec![]), + current_sorted_id: vec![], error: None, filter: Filter::new(), hidden_containers: vec![], + redraw: Arc::clone(redraw), sorted_by: None, } } @@ -186,6 +192,7 @@ impl AppData { /// sets the state to start if any filtering has occurred /// Also search in the "hidden" vec for items and insert back into the main containers vec fn filter_containers(&mut self) { + self.redraw.set_true(); let pre_len = self.get_container_len(); if !self.hidden_containers.is_empty() { @@ -289,6 +296,7 @@ impl AppData { /// Remove the sorted header & order, and sort by default - created datetime pub fn reset_sorted(&mut self) { self.set_sorted(None); + self.redraw.set_true(); } /// Sort containers based on a given header, if headings match, and already ascending, remove sorting @@ -309,10 +317,19 @@ impl AppData { self.sorted_by } + /// Get a vec of the containers ID's + fn get_current_ids(&self) -> Vec { + self.containers + .items + .iter() + .map(|i| i.id.clone()) + .collect::>() + } /// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending, /// If not sort set, then sort by created time pub fn sort_containers(&mut self) { if let Some((head, ord)) = self.sorted_by { + let pre_order = self.get_current_ids(); let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering { let item_ord = match ord { SortedOrder::Asc => (a, b), @@ -372,13 +389,19 @@ impl AppData { .then_with(|| item_ord.0.id.cmp(&item_ord.1.id)), } }; + self.containers.items.sort_by(sort_closure); - } else { + if pre_order != self.get_current_ids() { + self.redraw.set_true(); + } + } else if self.current_sorted_id != self.get_current_ids() { self.containers.items.sort_by(|a, b| { a.created .cmp(&b.created) .then_with(|| a.name.get().cmp(b.name.get())) }); + self.redraw.set_true(); + self.current_sorted_id = self.get_current_ids(); } } @@ -414,21 +437,25 @@ impl AppData { /// Select the first container pub fn containers_start(&mut self) { self.containers.start(); + self.redraw.set_true(); } /// select the last container pub fn containers_end(&mut self) { self.containers.end(); + self.redraw.set_true(); } /// Select the next container pub fn containers_next(&mut self) { self.containers.next(); + self.redraw.set_true(); } /// select the previous container pub fn containers_previous(&mut self) { self.containers.previous(); + self.redraw.set_true(); } /// Get ListState of containers @@ -521,6 +548,11 @@ impl AppData { self.get_selected_container().map(|i| i.id.clone()) } + /// Check if a given ID matches the currently selected container + pub fn is_selected_container(&self, id: &ContainerId) -> bool { + self.get_selected_container().is_some_and(|i| &i.id == id) + } + /// Get the Id and State for the currently selected container - used by the exec check method pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { self.get_selected_container() @@ -545,6 +577,7 @@ impl AppData { pub fn docker_controls_next(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.next(); + self.redraw.set_true(); } } @@ -552,6 +585,7 @@ impl AppData { pub fn docker_controls_previous(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.previous(); + self.redraw.set_true(); } } @@ -559,6 +593,7 @@ impl AppData { pub fn docker_controls_start(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.start(); + self.redraw.set_true(); } } @@ -566,6 +601,7 @@ impl AppData { pub fn docker_controls_end(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.end(); + self.redraw.set_true(); } } @@ -603,6 +639,7 @@ impl AppData { pub fn log_next(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.next(); + self.redraw.set_true(); } } @@ -610,6 +647,7 @@ impl AppData { pub fn log_previous(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.previous(); + self.redraw.set_true(); } } @@ -617,6 +655,7 @@ impl AppData { pub fn log_end(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.end(); + self.redraw.set_true(); } } @@ -624,6 +663,7 @@ impl AppData { pub fn log_start(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.start(); + self.redraw.set_true(); } } @@ -664,12 +704,14 @@ impl AppData { /// Remove single app_state error pub fn remove_error(&mut self) { self.error = None; + self.redraw.set_true(); } /// Insert single app_state error pub fn set_error(&mut self, error: AppError, gui_state: &Arc>, status: Status) { gui_state.lock().status_push(status); self.error = Some(error); + self.redraw.set_true(); } /// Check if the selected container is a dockerised version of oxker @@ -758,6 +800,9 @@ impl AppData { container.tx.update(tx); container.mem_limit.update(mem_limit); } + if self.is_selected_container(id) { + self.redraw.set_true(); + } self.sort_containers(); } @@ -793,6 +838,9 @@ impl AppData { // Check is some, else can cause out of bounds error, if containers get removed before a docker update if self.containers.items.get(index).is_some() { self.containers.items.remove(index); + if self.is_selected_container(id) { + self.redraw.set_true(); + } } } } @@ -872,6 +920,7 @@ impl AppData { } } } + // self.redraw.set_true("update_containers"); } } @@ -919,6 +968,9 @@ impl AppData { container.logs.end(); } } + if self.is_selected_container(id) { + self.redraw.set_true(); + } } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 2c4ed74..7a61bd2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -18,7 +18,7 @@ mod parse_config_file; pub struct Config { pub app_colors: AppColors, pub color_logs: bool, - pub docker_interval: u32, + pub docker_interval_ms: u32, pub gui: bool, pub host: Option, pub in_container: bool, @@ -38,7 +38,7 @@ impl From<&Args> for Config { Self { app_colors: AppColors::new(), color_logs: args.color, - docker_interval: args.docker_interval, + docker_interval_ms: args.docker_interval, gui: !args.gui, host: args.host.clone(), in_container: Self::check_if_in_container(), @@ -60,7 +60,7 @@ impl From for Config { Self { app_colors: AppColors::from(config_file.colors), color_logs: config_file.color_logs.unwrap_or(false), - docker_interval: config_file.docker_interval.unwrap_or(1000), + docker_interval_ms: config_file.docker_interval.unwrap_or(1000), gui: config_file.gui.unwrap_or(true), host: config_file.host, in_container: Self::check_if_in_container(), @@ -129,7 +129,7 @@ impl Config { /// make sure color_logs and raw_logs can't clash fn merge_args(mut self, config_from_cli: Self) -> Self { self.color_logs = config_from_cli.color_logs; - self.docker_interval = config_from_cli.docker_interval; + self.docker_interval_ms = config_from_cli.docker_interval_ms; self.gui = config_from_cli.gui; self.raw_logs = config_from_cli.raw_logs; self.show_self = config_from_cli.show_self; @@ -137,8 +137,8 @@ impl Config { self.show_timestamp = config_from_cli.show_timestamp; self.use_cli = config_from_cli.use_cli; - if config_from_cli.docker_interval < 1000 { - self.docker_interval = 1000; + if config_from_cli.docker_interval_ms < 1000 { + self.docker_interval_ms = 1000; } if let Some(host) = config_from_cli.host { diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index eeb082b..87382a5 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -400,7 +400,8 @@ impl DockerData { /// Send an update message every x ms, where x is the args.docker_interval fn heartbeat(config: &Config, docker_tx: Sender) { - let update_duration = std::time::Duration::from_millis(u64::from(config.docker_interval)); + let update_duration = + std::time::Duration::from_millis(u64::from(config.docker_interval_ms)); let mut now = std::time::Instant::now(); tokio::spawn(async move { loop { diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 01f1f70..fad84a6 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -625,7 +625,7 @@ impl InputHandler { self.gui_state.lock().status_push(Status::Help); } - self.gui_state.lock().get_intersect_panel(mouse_point); + self.gui_state.lock().check_panel_intersect(mouse_point); } _ => (), } diff --git a/src/main.rs b/src/main.rs index fb78459..2f2b2eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ mod exec; mod input_handler; mod ui; -use ui::{GuiState, Status, Ui}; +use ui::{GuiState, Redraw, Status, Ui}; use crate::docker_data::DockerMessage; @@ -98,9 +98,10 @@ fn handler_init( async fn main() { setup_tracing(); let config = config::Config::new(); + let redraw = Arc::new(Redraw::new()); - let app_data = Arc::new(Mutex::new(AppData::default(config.clone()))); - let gui_state = Arc::new(Mutex::new(GuiState::default())); + let app_data = Arc::new(Mutex::new(AppData::new(config.clone(), &redraw))); + let gui_state = Arc::new(Mutex::new(GuiState::new(&redraw))); let is_running = Arc::new(AtomicBool::new(true)); let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32); @@ -109,7 +110,7 @@ async fn main() { if config.gui { let (input_tx, input_rx) = tokio::sync::mpsc::channel(32); handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); - Ui::start(app_data, gui_state, input_tx, is_running).await; + Ui::start(app_data, gui_state, input_tx, is_running, redraw).await; } else { info!("in debug mode\n"); let mut now = std::time::Instant::now(); @@ -120,7 +121,7 @@ async fn main() { error!("{}", err); process::exit(1); } - if let Some(Ok(to_sleep)) = u128::from(config.docker_interval) + if let Some(Ok(to_sleep)) = u128::from(config.docker_interval_ms) .checked_sub(now.elapsed().as_millis()) .map(u64::try_from) { @@ -148,6 +149,8 @@ async fn main() { #[allow(clippy::unwrap_used)] mod tests { + use std::sync::Arc; + use bollard::service::{ContainerSummary, Port}; use crate::{ @@ -156,13 +159,14 @@ mod tests { RunningState, State, StatefulList, }, config::{AppColors, Config, Keymap}, + ui::Redraw, }; /// Default test config, has timestamps turned off pub fn gen_config() -> Config { Config { color_logs: false, - docker_interval: 1000, + docker_interval_ms: 1000, gui: true, host: None, show_std_err: false, @@ -200,8 +204,10 @@ mod tests { AppData { containers: StatefulList::new(containers.to_vec()), hidden_containers: vec![], + current_sorted_id: vec![], error: None, sorted_by: None, + redraw: Arc::new(Redraw::new()), filter: Filter::new(), config: gen_config(), } diff --git a/src/ui/draw_blocks/mod.rs b/src/ui/draw_blocks/mod.rs index 51d084c..26a662c 100644 --- a/src/ui/draw_blocks/mod.rs +++ b/src/ui/draw_blocks/mod.rs @@ -123,7 +123,7 @@ pub mod tests { use crate::{ app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts}, tests::{gen_appdata, gen_containers}, - ui::{draw_frame, GuiState}, + ui::{draw_frame, GuiState, Redraw}, }; use super::FrameData; @@ -194,7 +194,8 @@ pub mod tests { app_data.containers_start(); } - let gui_state = GuiState::default(); + let redraw = Arc::new(Redraw::new()); + let gui_state = GuiState::new(&redraw); let app_data = Arc::new(Mutex::new(app_data)); let gui_state = Arc::new(Mutex::new(gui_state)); diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 14ac715..f1abf2d 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -13,6 +13,8 @@ use crate::{ exec::ExecMode, }; +use super::Redraw; + #[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] pub enum SelectablePanel { #[default] @@ -171,22 +173,40 @@ pub enum Status { } /// Global gui_state, stored in an Arc -#[derive(Debug, Default)] +#[derive(Debug)] pub struct GuiState { delete_container: Option, exec_mode: Option, - loading_handle: Option>, - loading_index: u8, - loading_set: HashSet, intersect_delete: HashMap, intersect_heading: HashMap, intersect_help: Option, intersect_panel: HashMap, + loading_handle: Option>, + loading_index: u8, + loading_set: HashSet, + redraw: Arc, selected_panel: SelectablePanel, status: HashSet, pub info_box_text: Option<(String, Instant)>, } impl GuiState { + pub fn new(redraw: &Arc) -> Self { + Self { + delete_container: None, + exec_mode: None, + info_box_text: None, + intersect_delete: HashMap::new(), + intersect_heading: HashMap::new(), + intersect_help: None, + intersect_panel: HashMap::new(), + loading_handle: None, + loading_index: 0, + loading_set: HashSet::new(), + redraw: Arc::clone(redraw), + selected_panel: SelectablePanel::default(), + status: HashSet::new(), + } + } /// Clear panels hash map, so on resize can fix the sizes for mouse clicks pub fn clear_area_map(&mut self) { self.intersect_panel.clear(); @@ -198,7 +218,7 @@ impl GuiState { } /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels - pub fn get_intersect_panel(&mut self, rect: Rect) { + pub fn check_panel_intersect(&mut self, rect: Rect) { if let Some(data) = self .intersect_panel .iter() @@ -207,6 +227,7 @@ impl GuiState { .first() { self.selected_panel = *data.0; + self.redraw.set_true(); } } @@ -299,6 +320,7 @@ impl GuiState { } _ => (), } + self.redraw.set_true(); } /// Inset the ExecMode into self, and set the Status as exec @@ -307,6 +329,7 @@ impl GuiState { pub fn set_exec_mode(&mut self, mode: ExecMode) { self.exec_mode = Some(mode); self.status.insert(Status::Exec); + self.redraw.set_true(); } pub fn get_exec_mode(&self) -> Option { @@ -316,22 +339,22 @@ impl GuiState { /// Insert a gui_status into the current gui_status HashSet /// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead pub fn status_push(&mut self, status: Status) { - match status { - Status::Exec => (), - _ => { - self.status.insert(status); - } + if status != Status::Exec { + self.status.insert(status); + self.redraw.set_true(); } } /// Change to next selectable panel pub fn next_panel(&mut self) { self.selected_panel = self.selected_panel.next(); + self.redraw.set_true(); } /// Change to previous selectable panel pub fn previous_panel(&mut self) { self.selected_panel = self.selected_panel.prev(); + self.redraw.set_true(); } /// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array @@ -342,6 +365,7 @@ impl GuiState { self.loading_index += 1; } self.loading_set.insert(uuid); + self.redraw.set_true(); } pub fn is_loading(&self) -> bool { @@ -374,6 +398,7 @@ impl GuiState { /// Stop the loading_spin function, and reset gui loading status pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) { self.loading_set.remove(&loading_uuid); + self.redraw.set_true(); if self.loading_set.is_empty() { self.loading_index = 0; if let Some(h) = &self.loading_handle { @@ -386,10 +411,12 @@ impl GuiState { /// Set info box content pub fn set_info_box(&mut self, text: &str) { self.info_box_text = Some((text.to_owned(), std::time::Instant::now())); + self.redraw.set_true(); } /// Remove info box content pub fn reset_info_box(&mut self) { self.info_box_text = None; + self.redraw.set_true(); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 812acaa..d103c22 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -23,6 +23,8 @@ use tracing::error; mod color_match; mod draw_blocks; mod gui_state; +mod redraw; +pub use redraw::Redraw; pub use self::color_match::*; pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; @@ -37,16 +39,19 @@ use crate::{ input_handler::InputMessages, }; -const POLL_RATE: Duration = std::time::Duration::from_millis(100); +const POLL_RATE: Duration = std::time::Duration::from_millis(50); + +// could have a render struct, which takes in poll rate, and docker pub struct Ui { app_data: Arc>, + cursor_position: Position, gui_state: Arc>, input_tx: Sender, is_running: Arc, now: Instant, + redraw: Arc, terminal: Terminal>, - cursor_position: Position, } impl Ui { @@ -68,6 +73,7 @@ impl Ui { gui_state: Arc>, input_tx: Sender, is_running: Arc, + redraw: Arc, ) { if let Ok(mut terminal) = Self::setup_terminal() { let cursor_position = terminal.get_cursor_position().unwrap_or_default(); @@ -78,6 +84,7 @@ impl Ui { input_tx, is_running, now: Instant::now(), + redraw, terminal, }; if let Err(e) = ui.draw_ui().await { @@ -126,18 +133,18 @@ impl Ui { let mut seconds = 5; let colors = self.app_data.lock().config.app_colors; let keymap = self.app_data.lock().config.keymap.clone(); - let mut render = true; + let mut redraw = true; loop { if self.now.elapsed() >= std::time::Duration::from_secs(1) { seconds -= 1; self.now = Instant::now(); - render = true; + redraw = true; if seconds < 1 { break; } } - if render + if redraw && self .terminal .draw(|f| { @@ -153,7 +160,7 @@ impl Ui { { return Err(AppError::Terminal); } - render = false; + redraw = false; std::thread::sleep(POLL_RATE); } Ok(()) @@ -178,25 +185,41 @@ impl Ui { self.gui_state.lock().status_del(Status::Exec); } + /// Use the previously redrawn time, the current time, the docker_interval, and the redraw struct, to calculate + /// if the screen should be redrawn or not + fn should_redraw(&self, previous: &mut Instant, docker_interval_ms: u128) -> bool { + let result = self.redraw.swap() || previous.elapsed().as_millis() >= docker_interval_ms; + if result { + *previous = std::time::Instant::now(); + } + result + } + /// The loop for drawing the main UI to the terminal async fn gui_loop(&mut self) -> Result<(), AppError> { let colors = self.app_data.lock().config.app_colors; let keymap = self.app_data.lock().config.keymap.clone(); - while self.is_running.load(Ordering::SeqCst) { - let fd = FrameData::from(&*self); - let exec = fd.status.contains(&Status::Exec); - if exec { - self.exec().await; - } + let docker_interval_ms = u128::from(self.app_data.lock().config.docker_interval_ms); + let mut drawn_at = std::time::Instant::now(); - if self - .terminal - .draw(|frame| { - draw_frame(&self.app_data, colors, &keymap, frame, &fd, &self.gui_state); - }) - .is_err() - { - return Err(AppError::Terminal); + while self.is_running.load(Ordering::SeqCst) { + if self.should_redraw(&mut drawn_at, docker_interval_ms) { + let fd = FrameData::from(&*self); + + let exec = fd.status.contains(&Status::Exec); + if exec { + self.exec().await; + } + + if self + .terminal + .draw(|frame| { + draw_frame(&self.app_data, colors, &keymap, frame, &fd, &self.gui_state); + }) + .is_err() + { + return Err(AppError::Terminal); + } } if crossterm::event::poll(POLL_RATE).unwrap_or(false) { diff --git a/src/ui/redraw.rs b/src/ui/redraw.rs new file mode 100644 index 0000000..32e21a1 --- /dev/null +++ b/src/ui/redraw.rs @@ -0,0 +1,25 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +#[derive(Debug)] +pub struct Redraw(AtomicBool); + +impl Redraw { + pub const fn new() -> Self { + Self(AtomicBool::new(true)) + } + + pub fn set_true(&self) { + self.0.store(true, Ordering::SeqCst); + } + + /// Return the value of the self, and set to false + pub fn swap(&self) -> bool { + match self + .0 + .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + { + Ok(previous_value) => previous_value, + Err(current_value) => current_value, + } + } +}