Skip to content

Commit

Permalink
feat(cli): ✨ added single-threaded mode for the CLI
Browse files Browse the repository at this point in the history
This can be enabled by setting `PERSEUS_CLI_SEQUENTIAL`.

Closes #11.
  • Loading branch information
arctic-hen7 committed Sep 17, 2021
1 parent 379d549 commit 5cb465a
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 10 deletions.
4 changes: 4 additions & 0 deletions packages/perseus-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ fn core(dir: PathBuf) -> Result<i32> {
let _executable_name = prog_args.remove(0);
// Check the user's environment to make sure they have prerequisites
check_env()?;
// Warn the user if they're using the CLI single-threaded mode
if env::var("PERSEUS_CLI_SEQUENTIAL").is_ok() {
writeln!(stdout, "Note: the Perseus CLI is running in single-threaded mode, which is less performant on most modern systems. You can switch to multi-threaded mode by unsetting the 'PERSEUS_CLI_SEQUENTIAL' environment variable. If you've deliberately enabled single-threaded mode, you can safely ignore this.\n").expect("Failed to write to stdout.");
}
// Check for special arguments
if matches!(prog_args.get(0), Some(_)) {
if prog_args[0] == "-v" || prog_args[0] == "--version" {
Expand Down
15 changes: 8 additions & 7 deletions packages/perseus-cli/src/build.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::cmd::{cfg_spinner, run_stage};
use crate::errors::*;
use crate::thread::{spawn_thread, ThreadHandle};
use console::{style, Emoji};
use indicatif::{MultiProgress, ProgressBar};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::thread::{self, JoinHandle};

// Emojis for stages
static GENERATING: Emoji<'_, '_> = Emoji("🔨", "");
Expand Down Expand Up @@ -38,17 +38,18 @@ pub fn finalize(target: &Path) -> Result<()> {
Ok(())
}

// This literally only exists to avoid type complexity warnings in the `build_internal`'s return type
type ThreadHandle = JoinHandle<Result<i32>>;

/// Actually builds the user's code, program arguments having been interpreted. This needs to know how many steps there are in total
/// because the serving logic also uses it. This also takes a `MultiProgress` to interact with so it can be used truly atomically.
/// This returns handles for waiting on the component threads so we can use it composably.
#[allow(clippy::type_complexity)]
pub fn build_internal(
dir: PathBuf,
spinners: &MultiProgress,
num_steps: u8,
) -> Result<(ThreadHandle, ThreadHandle)> {
) -> Result<(
ThreadHandle<impl FnOnce() -> Result<i32>, Result<i32>>,
ThreadHandle<impl FnOnce() -> Result<i32>, Result<i32>>,
)> {
let target = dir.join(".perseus");

// Static generation message
Expand All @@ -72,7 +73,7 @@ pub fn build_internal(
let wb_spinner = spinners.insert(1, ProgressBar::new_spinner());
let wb_spinner = cfg_spinner(wb_spinner, &wb_msg);
let wb_target = target.clone();
let sg_thread = thread::spawn(move || {
let sg_thread = spawn_thread(move || {
handle_exit_code!(run_stage(
vec![&format!(
"{} run",
Expand All @@ -85,7 +86,7 @@ pub fn build_internal(

Ok(0)
});
let wb_thread = thread::spawn(move || {
let wb_thread = spawn_thread(move || {
handle_exit_code!(run_stage(
vec![&format!(
"{} build --target web",
Expand Down
1 change: 1 addition & 0 deletions packages/perseus-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod errors;
mod help;
mod prepare;
mod serve;
mod thread;

mod extraction;

Expand Down
6 changes: 3 additions & 3 deletions packages/perseus-cli/src/serve.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use crate::build::{build_internal, finalize};
use crate::cmd::{cfg_spinner, run_stage};
use crate::errors::*;
use crate::thread::{spawn_thread, ThreadHandle};
use console::{style, Emoji};
use indicatif::{MultiProgress, ProgressBar};
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};

// Emojis for stages
static BUILDING_SERVER: Emoji<'_, '_> = Emoji("📡", "");
Expand All @@ -34,7 +34,7 @@ fn build_server(
spinners: &MultiProgress,
did_build: bool,
exec: Arc<Mutex<String>>,
) -> Result<JoinHandle<Result<i32>>> {
) -> Result<ThreadHandle<impl FnOnce() -> Result<i32>, Result<i32>>> {
let num_steps = match did_build {
true => 4,
false => 2,
Expand All @@ -55,7 +55,7 @@ fn build_server(
let sb_spinner = spinners.insert(num_steps - 1, ProgressBar::new_spinner());
let sb_spinner = cfg_spinner(sb_spinner, &sb_msg);
let sb_target = target.clone();
let sb_thread = thread::spawn(move || {
let sb_thread = spawn_thread(move || {
let (stdout, _stderr) = handle_exit_code!(run_stage(
vec![&format!(
// This sets Cargo to tell us everything, including the executable path to the server
Expand Down
59 changes: 59 additions & 0 deletions packages/perseus-cli/src/thread.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::env;
use std::thread::{self, JoinHandle};

/// Spawns a new thread with the given code, or executes it directly if the environment variable `PERSEUS_CLI_SEQUENTIAL` is set to
/// any valid (Unicode) value. Multithreading is the default.
pub fn spawn_thread<F, T>(f: F) -> ThreadHandle<F, T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
let single = env::var("PERSEUS_CLI_SEQUENTIAL").is_ok();
if single {
ThreadHandle {
join_handle: None,
f: Some(f),
}
} else {
let join_handle = thread::spawn(f);
ThreadHandle {
join_handle: Some(join_handle),
f: None,
}
}
}

/// An abstraction over a `JoinHandle` in a multithreaded case, or just a similar interface that will immediately return if otherwise.
/// This allows the interfaces for multithreading and single-threading to be basically identical.
pub struct ThreadHandle<F, T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
/// If multithreaded, this is the join handle.
join_handle: Option<JoinHandle<T>>,
// If single-threaded, this is the output (it's already been executed).
f: Option<F>,
}
impl<F, T> ThreadHandle<F, T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
/// Waits for the 'thread' to complete, properly if it's multithreaded, or by direct execution if it's single-threaded.
pub fn join(
self,
) -> Result<T, std::boxed::Box<(dyn std::any::Any + std::marker::Send + 'static)>> {
if let Some(join_handle) = self.join_handle {
join_handle.join()
} else if let Some(f) = self.f {
let output = f();
Ok(output)
} else {
unreachable!();
}
}
}

0 comments on commit 5cb465a

Please sign in to comment.