Skip to content

Commit

Permalink
feat: implement multithreading in UI for better performance
Browse files Browse the repository at this point in the history
Now all UI frontends follow an update loop and input as logging is
sent through mpsc messaging. Replacing the slow io write waiting in
between commands with way faster mpsc send waiting.

Additionally refactored the way frontends work by removing the trait
Frontend all together. As now they don't require to follow that
architecture and can just receive mpsc messages through composition
rather than trait "inheritance".

TLDR, this change will prevent IO bottlenecking when logging to
stdout/performing TUI rendering.
  • Loading branch information
TheAlexDev23 committed Feb 29, 2024
1 parent 4611c54 commit 44b9334
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 216 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ crossterm = "0.27.0"
indicatif = "0.17.8"
tokio = { version = "1", features = ["full"] }
async-recursion = "1.0.5"
async-trait = "0.1"
rayon = "1.8.1"
29 changes: 14 additions & 15 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ pub async fn install_packages<EFind: Display, EDatabase: Display>(
) -> Result<Vec<Action>, InstallError<EDatabase, EFind>> {
let mut actions: LinkedHashSet<Action> = LinkedHashSet::new();

progress::increment_target(ProgressType::Packages, packages.len() as i32);
progress::increment_target(ProgressType::Packages, packages.len() as i32).await;

for package_name in packages.iter() {
actions.extend(install_package(package_name, package_finder, reinstall_options, db).await?);

progress::increment_completed(ProgressType::Packages, 1);
progress::increment_completed(ProgressType::Packages, 1).await;
}

Ok(actions.keys().cloned().collect())
Expand All @@ -55,11 +55,11 @@ pub async fn remove_packages<EDatabase: Display>(
) -> Result<Vec<Action>, RemoveError<EDatabase>> {
let mut actions: LinkedHashSet<Action> = LinkedHashSet::new();

progress::increment_target(ProgressType::Packages, package_names.len() as i32);
progress::increment_target(ProgressType::Packages, package_names.len() as i32).await;

for package_name in package_names.into_iter() {
actions.extend(remove_package(&package_name, recursive, db)?);
progress::increment_completed(ProgressType::Packages, 1);
actions.extend(remove_package(&package_name, recursive, db).await?);
progress::increment_completed(ProgressType::Packages, 1).await;
}

Ok(actions.keys().cloned().collect())
Expand Down Expand Up @@ -204,20 +204,22 @@ async fn install_package<EFind: Display, EDatabase: Display>(
progress::increment_target(
ProgressType::Packages,
remote_package.dependencies.len() as i32,
);
)
.await;

for dependency in remote_package.dependencies.iter() {
actions.extend(install_package(dependency, package_finder, reinstall_options, db).await?);

progress::increment_completed(ProgressType::Packages, 1);
progress::increment_completed(ProgressType::Packages, 1).await;
}

actions.insert(Action::Install(remote_package), ());

Ok(actions)
}

fn remove_package<EDatabase: Display>(
#[async_recursion(?Send)]
async fn remove_package<EDatabase: Display>(
package_name: &str,
recursive: bool,
db: &mut impl PackagesDb<GetError = EDatabase>,
Expand Down Expand Up @@ -246,16 +248,13 @@ fn remove_package<EDatabase: Display>(
if !depending_packages.is_empty() {
if recursive {
info!("Found depending packages, uninstalling...");
progress::increment_target(ProgressType::Packages, depending_packages.len() as i32);
progress::increment_target(ProgressType::Packages, depending_packages.len() as i32)
.await;

for dependency in depending_packages.iter() {
actions.extend(remove_package(
&dependency.package_data.name,
recursive,
db,
)?);
actions.extend(remove_package(&dependency.package_data.name, recursive, db).await?);

progress::increment_completed(ProgressType::Packages, 1);
progress::increment_completed(ProgressType::Packages, 1).await;
}
} else {
let depending_packages: Vec<String> = depending_packages
Expand Down
7 changes: 4 additions & 3 deletions src/commands/tests/mock_progressbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use crate::progress::{Progress, ProgressType};

pub struct MockProgressbar;

#[async_trait::async_trait]
impl Progress for MockProgressbar {
fn increment_target(&mut self, _progress_type: ProgressType, _amount: i32) {}
fn increment_completed(&mut self, _progress_type: ProgressType, _amount: i32) {}
fn set_comleted(&mut self, _progress_type: ProgressType) {}
async fn increment_target(&mut self, _progress_type: ProgressType, _amount: i32) {}
async fn increment_completed(&mut self, _progress_type: ProgressType, _amount: i32) {}
async fn set_comleted(&mut self, _progress_type: ProgressType) {}
}
60 changes: 25 additions & 35 deletions src/frontends.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::sync::{Mutex, MutexGuard};
use std::sync::Arc;

use crate::action::Action;

pub use stdout::StdFrontend;
pub use tui::TuiFrontend;
use messaging::UIWriteHandle;

mod stdout;
mod tui;
pub mod messaging;
pub mod stdout;
pub mod tui;

#[derive(Clone)]
pub enum MessageColor {
White,
Cyan,
Expand All @@ -16,44 +17,33 @@ pub enum MessageColor {
Purple,
}

static mut CURRENT_FRONTEND: Option<Mutex<Box<dyn Frontend>>> = None;
static mut UI_MESSENGER: Option<Arc<UIWriteHandle>> = None;

pub trait Frontend {
fn refresh(&mut self);
fn display_message(&mut self, message: String, color: &MessageColor);
fn display_action(&mut self, action: &Action);
fn set_progressbar(&mut self, percentage: f32);
fn exit(&mut self);
}

pub fn set_boxed_frontend(frontend: Box<dyn Frontend>) {
pub fn set_ui_messenger(messenger: UIWriteHandle) {
unsafe {
CURRENT_FRONTEND = Some(Mutex::new(frontend));
UI_MESSENGER = Some(Arc::new(messenger));
}
}

pub fn display_message(message: String, color: &MessageColor) {
get_frontend().display_message(message, color);
pub async fn display_message(message: String, color: &MessageColor) -> Option<()> {
get_messenger()?.display_message(message, color).await;
Some(())
}
pub fn display_action(action: &Action) {
get_frontend().display_action(action);
pub async fn display_action(action: &Action) -> Option<()> {
get_messenger()?.display_action(action).await;
Some(())
}
pub fn set_progressbar(percentage: f32) {
get_frontend().set_progressbar(percentage);
pub async fn set_progressbar(percentage: f32) -> Option<()> {
get_messenger()?.set_progressbar(percentage).await;
Some(())
}
pub fn exit() {
get_frontend().exit();
pub async fn exit() -> Option<()> {
let messenger = get_messenger()?;
messenger.exit().await;
messenger.exit_finish.lock().await.recv().await;
Some(())
}

fn get_frontend<'a>() -> MutexGuard<'a, Box<dyn Frontend>> {
unsafe {
// Need to lock instead of get_mut because otherwise weird graphic fuckery happens that I don't really
// understand
#[allow(clippy::mut_mutex_lock)]
CURRENT_FRONTEND
.as_mut()
.unwrap()
.lock()
.expect("Could not lock frontend instance.")
}
fn get_messenger<'a>() -> Option<&'a UIWriteHandle> {
unsafe { UI_MESSENGER.as_deref() }
}
72 changes: 72 additions & 0 deletions src/frontends/messaging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::sync::Mutex;

use crate::Action;

use super::MessageColor;

pub struct UIWriteHandle {
messages: Mutex<UnboundedSender<(String, MessageColor)>>,
actions: Mutex<UnboundedSender<Action>>,
progressbar: Mutex<UnboundedSender<f32>>,
exit: Mutex<UnboundedSender<()>>,

/// The frontend will need to send to this receiver through [UIReadHandle::exit_finish]
/// once the [Self::exit] procedure is finished.
pub exit_finish: Mutex<UnboundedReceiver<()>>,
}

pub struct UIReadHandle {
pub messages: UnboundedReceiver<(String, MessageColor)>,
pub actions: UnboundedReceiver<Action>,
pub progressbar: UnboundedReceiver<f32>,
pub exit: UnboundedReceiver<()>,
pub exit_finish: Mutex<UnboundedSender<()>>,
}

pub fn generate_message_pair() -> (UIWriteHandle, UIReadHandle) {
let (mw, mr) = mpsc::unbounded_channel();
let (aw, ar) = mpsc::unbounded_channel();
let (pw, pr) = mpsc::unbounded_channel();
let (ew, er) = mpsc::unbounded_channel();
let (efw, efr) = mpsc::unbounded_channel();

(
UIWriteHandle {
messages: mw.into(),
actions: aw.into(),
progressbar: pw.into(),
exit: ew.into(),
exit_finish: efr.into(),
},
UIReadHandle {
messages: mr,
actions: ar,
progressbar: pr,
exit: er,
exit_finish: efw.into(),
},
)
}

impl UIWriteHandle {
pub async fn display_message(&self, message: String, color: &MessageColor) {
self.messages
.lock()
.await
.send((message, color.clone()))
.unwrap();
}

pub async fn display_action(&self, action: &Action) {
self.actions.lock().await.send(action.clone()).unwrap();
}

pub async fn set_progressbar(&self, percentage: f32) {
self.progressbar.lock().await.send(percentage).unwrap();
}

pub async fn exit(&self) {
self.exit.lock().await.send(()).unwrap();
}
}
70 changes: 47 additions & 23 deletions src/frontends/stdout.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
use std::io;

use tokio::select;

use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};

use super::{Frontend, MessageColor};
use crate::frontends::messaging::UIReadHandle;

use super::MessageColor;

struct StdHandle {
messaging_handle: UIReadHandle,

pub struct StdFrontend {
terminal_width: u16,
progressbar: ProgressBar,
}

impl StdFrontend {
pub fn new() -> Result<StdFrontend, io::Error> {
pub fn init(read_handle: UIReadHandle) -> Result<(), io::Error> {
let mut handle = StdHandle::init(read_handle)?;
tokio::spawn(async move { handle.update_cycle().await });

Ok(())
}

impl StdHandle {
pub fn init(read_handle: UIReadHandle) -> Result<StdHandle, io::Error> {
let (width, _) = crossterm::terminal::size()?;
let progressbar = ProgressBar::new(width as u64);
progressbar.set_style(
Expand All @@ -20,34 +33,45 @@ impl StdFrontend {
.progress_chars("██ "),
);

Ok(StdFrontend {
Ok(StdHandle {
messaging_handle: read_handle,
terminal_width: width,
progressbar,
})
}
}

impl Frontend for StdFrontend {
fn refresh(&mut self) {}

fn display_message(&mut self, message: String, color: &super::MessageColor) {
match color {
MessageColor::White => self.progressbar.println(format!("{}", message.white())),
MessageColor::Cyan => self.progressbar.println(format!("{}", message.cyan())),
MessageColor::Green => self.progressbar.println(format!("{}", message.green())),
MessageColor::Yellow => self.progressbar.println(format!("{}", message.yellow())),
MessageColor::Purple => self.progressbar.println(format!("{}", message.purple())),
pub(self) async fn update_cycle(&mut self) {
loop {
if self.handle_input().await {
return;
}
}
}

fn display_action(&mut self, _: &crate::action::Action) {}
async fn handle_input(&mut self) -> bool {
select! {
Some((message, color)) = self.messaging_handle.messages.recv() => {
match color {
MessageColor::White => self.progressbar.println(format!("{}", message.white())),
MessageColor::Cyan => self.progressbar.println(format!("{}", message.cyan())),
MessageColor::Green => self.progressbar.println(format!("{}", message.green())),
MessageColor::Yellow => self.progressbar.println(format!("{}", message.yellow())),
MessageColor::Purple => self.progressbar.println(format!("{}", message.purple())),
}

fn set_progressbar(&mut self, percentage: f32) {
self.progressbar
.set_position((self.terminal_width as f32 * percentage) as u64)
}
false
}
Some(percentage) = self.messaging_handle.progressbar.recv() => {
self.progressbar
.set_position((self.terminal_width as f32 * percentage) as u64);

fn exit(&mut self) {
self.progressbar.finish_and_clear();
false
}
Some(_) = self.messaging_handle.exit.recv() => {
self.progressbar.finish_and_clear();

true
}
}
}
}
Loading

0 comments on commit 44b9334

Please sign in to comment.