perf(network): Improved performance and reactiveness of the UI by speeding up network requests and clearing the channel whenever a request is cancelled/the UI is routing

This commit is contained in:
2024-11-06 14:52:48 -07:00
parent a708f71d57
commit 8c90221a81
6 changed files with 130 additions and 82 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ impl<'a> App<'a> {
} }
if self.is_routing { if self.is_routing {
if self.is_loading && !self.should_refresh { if !self.should_refresh {
self.cancellation_token.cancel(); self.cancellation_token.cancel();
} }
-1
View File
@@ -582,7 +582,6 @@ mod tests {
async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (mut app, mut sync_network_rx) = construct_app_unit(); let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_routing = true; app.is_routing = true;
app.is_loading = true;
app.should_refresh = false; app.should_refresh = false;
app app
+25 -5
View File
@@ -6,6 +6,7 @@ use std::panic::PanicHookInfo;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use std::{io, panic, process}; use std::{io, panic, process};
use anyhow::anyhow; use anyhow::anyhow;
@@ -20,11 +21,12 @@ use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
}; };
use log::error; use log::{error, warn};
use network::NetworkTrait; use network::NetworkTrait;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::Terminal; use ratatui::Terminal;
use reqwest::{Certificate, Client}; use reqwest::{Certificate, Client};
use tokio::select;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -144,9 +146,24 @@ async fn start_networking(
) { ) {
let mut network = Network::new(app, cancellation_token, client); let mut network = Network::new(app, cancellation_token, client);
while let Some(network_event) = network_rx.recv().await { loop {
if let Err(e) = network.handle_network_event(network_event).await { select! {
error!("Encountered an error handling network event: {e:?}"); Some(network_event) = network_rx.recv() => {
if let Err(e) = network.handle_network_event(network_event).await {
error!("Encountered an error handling network event: {e:?}");
}
}
_ = network.cancellation_token.cancelled() => {
warn!("Clearing network channel");
while network_rx.try_recv().is_ok() {
// Discard the message
}
{
/* Wrapped in its own block so we don't lock the app arc early,
so UI is still processed */
network.reset_cancellation_token().await;
}
}
} }
} }
} }
@@ -228,7 +245,10 @@ fn load_config(path: &str) -> Result<AppConfig> {
} }
fn build_network_client(config: &AppConfig) -> Client { fn build_network_client(config: &AppConfig) -> Client {
let mut client_builder = Client::builder(); let mut client_builder = Client::builder()
.pool_max_idle_per_host(10)
.http2_keep_alive_interval(Duration::from_secs(5))
.tcp_keepalive(Duration::from_secs(5));
if let Some(ref cert_path) = config.radarr.ssl_cert_path { if let Some(ref cert_path) = config.radarr.ssl_cert_path {
let cert = create_cert(cert_path, "Radarr"); let cert = create_cert(cert_path, "Radarr");
+8 -4
View File
@@ -40,7 +40,7 @@ pub trait NetworkTrait {
#[derive(Clone)] #[derive(Clone)]
pub struct Network<'a, 'b> { pub struct Network<'a, 'b> {
client: Client, client: Client,
cancellation_token: CancellationToken, pub cancellation_token: CancellationToken,
pub app: &'a Arc<Mutex<App<'b>>>, pub app: &'a Arc<Mutex<App<'b>>>,
} }
@@ -74,6 +74,13 @@ impl<'a, 'b> Network<'a, 'b> {
} }
} }
pub(super) async fn reset_cancellation_token(&mut self) {
let mut app = self.app.lock().await;
self.cancellation_token = app.reset_cancellation_token();
app.should_refresh = true;
app.is_loading = false;
}
async fn handle_request<B, R>( async fn handle_request<B, R>(
&mut self, &mut self,
request_props: RequestProps<B>, request_props: RequestProps<B>,
@@ -89,9 +96,6 @@ impl<'a, 'b> Network<'a, 'b> {
select! { select! {
_ = self.cancellation_token.cancelled() => { _ = self.cancellation_token.cancelled() => {
warn!("Received Cancel request. Cancelling request to: {request_uri}"); warn!("Received Cancel request. Cancelling request to: {request_uri}");
let mut app = self.app.lock().await;
self.cancellation_token = app.reset_cancellation_token();
app.is_loading = false;
Ok(R::default()) Ok(R::default())
} }
resp = self.call_api(request_props).await.send() => { resp = self.call_api(request_props).await.send() => {
+21 -1
View File
@@ -181,11 +181,31 @@ mod tests {
assert!(!async_server.matched_async().await); assert!(!async_server.matched_async().await);
assert!(app_arc.lock().await.error.text.is_empty()); assert!(app_arc.lock().await.error.text.is_empty());
assert!(!network.cancellation_token.is_cancelled());
assert!(resp.is_ok()); assert!(resp.is_ok());
assert_eq!(resp.unwrap(), Test::default()); assert_eq!(resp.unwrap(), Test::default());
} }
#[tokio::test]
async fn test_reset_cancellation_token() {
let cancellation_token = CancellationToken::new();
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
let app_arc = Arc::new(Mutex::new(App::new(
tx,
AppConfig::default(),
cancellation_token.clone(),
)));
app_arc.lock().await.should_refresh = false;
app_arc.lock().await.is_loading = true;
let mut network = Network::new(&app_arc, cancellation_token, Client::new());
network.cancellation_token.cancel();
network.reset_cancellation_token().await;
assert!(!network.cancellation_token.is_cancelled());
assert!(app_arc.lock().await.should_refresh);
assert!(!app_arc.lock().await.is_loading);
}
#[tokio::test] #[tokio::test]
async fn test_handle_request_get_invalid_body() { async fn test_handle_request_get_invalid_body() {
let mut server = Server::new_async().await; let mut server = Server::new_async().await;
+75 -70
View File
@@ -286,83 +286,88 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} }
fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let is_loading = app.is_loading && app.data.radarr_data.movie_details_modal.is_none(); match app.data.radarr_data.movie_details_modal.as_mut() {
let cast_row_mapping = |cast_member: &Credit| { Some(movie_details_modal) if !app.is_loading => {
let Credit { let cast_row_mapping = |cast_member: &Credit| {
person_name, let Credit {
character, person_name,
.. character,
} = cast_member; ..
} = cast_member;
Row::new(vec![ Row::new(vec![
Cell::from(person_name.to_owned()), Cell::from(person_name.to_owned()),
Cell::from(character.clone().unwrap_or_default()), Cell::from(character.clone().unwrap_or_default()),
]) ])
.success() .success()
}; };
let content = Some( let content = Some(&mut movie_details_modal.movie_cast);
&mut app let help_footer = app
.data .data
.radarr_data .radarr_data
.movie_details_modal .movie_info_tabs
.as_mut() .get_active_tab_contextual_help();
.unwrap() let cast_table = ManagarrTable::new(content, cast_row_mapping)
.movie_cast, .block(layout_block_top_border())
); .footer(help_footer)
let help_footer = app .loading(app.is_loading)
.data .headers(["Cast Member", "Character"])
.radarr_data .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
.movie_info_tabs
.get_active_tab_contextual_help();
let cast_table = ManagarrTable::new(content, cast_row_mapping)
.block(layout_block_top_border())
.footer(help_footer)
.loading(is_loading)
.headers(["Cast Member", "Character"])
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
f.render_widget(cast_table, area); f.render_widget(cast_table, area);
}
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
layout_block_top_border(),
),
area,
),
}
} }
fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let is_loading = app.is_loading && app.data.radarr_data.movie_details_modal.is_none(); match app.data.radarr_data.movie_details_modal.as_mut() {
let crew_row_mapping = |crew_member: &Credit| { Some(movie_details_modal) if !app.is_loading => {
let Credit { let crew_row_mapping = |crew_member: &Credit| {
person_name, let Credit {
job, person_name,
department, job,
.. department,
} = crew_member; ..
} = crew_member;
Row::new(vec![ Row::new(vec![
Cell::from(person_name.to_owned()), Cell::from(person_name.to_owned()),
Cell::from(job.clone().unwrap_or_default()), Cell::from(job.clone().unwrap_or_default()),
Cell::from(department.clone().unwrap_or_default()), Cell::from(department.clone().unwrap_or_default()),
]) ])
.success() .success()
}; };
let content = Some( let content = Some(&mut movie_details_modal.movie_crew);
&mut app let help_footer = app
.data .data
.radarr_data .radarr_data
.movie_details_modal .movie_info_tabs
.as_mut() .get_active_tab_contextual_help();
.unwrap() let crew_table = ManagarrTable::new(content, crew_row_mapping)
.movie_crew, .block(layout_block_top_border())
); .loading(app.is_loading)
let help_footer = app .headers(["Crew Member", "Job", "Department"])
.data .constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3))
.radarr_data .footer(help_footer);
.movie_info_tabs
.get_active_tab_contextual_help();
let crew_table = ManagarrTable::new(content, crew_row_mapping)
.block(layout_block_top_border())
.loading(is_loading)
.headers(["Crew Member", "Job", "Department"])
.constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3))
.footer(help_footer);
f.render_widget(crew_table, area); f.render_widget(crew_table, area);
}
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
layout_block_top_border(),
),
area,
),
}
} }
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {