Skip to content

Commit

Permalink
feat: add scan subcommand (#6788)
Browse files Browse the repository at this point in the history
### Description

Adds a scan command that automates common optimisations so that you can
get the most out of turbo.

Closes TURBO-1912
  • Loading branch information
arlyon authored Mar 8, 2024
1 parent c6ffc44 commit 1e25520
Show file tree
Hide file tree
Showing 14 changed files with 753 additions and 32 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion crates/turborepo-lib/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use turborepo_ui::UI;

use crate::{
commands::{
bin, daemon, generate, info, link, login, logout, prune, run, telemetry, unlink,
bin, daemon, generate, info, link, login, logout, prune, run, scan, telemetry, unlink,
CommandBase,
},
get_version,
Expand Down Expand Up @@ -465,6 +465,9 @@ pub enum Command {
#[serde(flatten)]
command: Option<TelemetryCommand>,
},
/// Turbo your monorepo by running a number of 'repo lints' to
/// identify common issues, suggest fixes, and improve performance.
Scan {},
#[clap(hide = true)]
Info {
workspace: Option<String>,
Expand Down Expand Up @@ -1093,6 +1096,14 @@ pub async fn run(
telemetry::configure(command, &mut base, child_event);
Ok(0)
}
Command::Scan {} => {
let base = CommandBase::new(cli_args.clone(), repo_root, version, ui);
if scan::run(base).await {
Ok(0)
} else {
Ok(1)
}
}
Command::Info { workspace, json } => {
CommandEventBuilder::new("info")
.with_parent(&root_telemetry)
Expand Down
14 changes: 7 additions & 7 deletions crates/turborepo-lib/src/commands/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ use std::{
#[cfg(not(test))]
use console::Style;
use console::StyledObject;
use dialoguer::Confirm;
#[cfg(not(test))]
use dialoguer::FuzzySelect;
use dialoguer::{theme::ColorfulTheme, Confirm};
use dirs_next::home_dir;
#[cfg(test)]
use rand::Rng;
use thiserror::Error;
use turborepo_api_client::{CacheClient, Client};
#[cfg(not(test))]
use turborepo_ui::CYAN;
use turborepo_ui::{BOLD, GREY, UNDERLINE};
use turborepo_ui::{DialoguerTheme, BOLD, GREY, UNDERLINE};
use turborepo_vercel_api::{CachingStatus, Space, Team};

use crate::{
Expand Down Expand Up @@ -374,7 +374,7 @@ pub async fn link(
}

fn should_enable_caching() -> Result<bool, Error> {
let theme = ColorfulTheme::default();
let theme = DialoguerTheme::default();

Confirm::with_theme(&theme)
.with_prompt(
Expand Down Expand Up @@ -410,12 +410,12 @@ fn select_team<'a>(
let mut team_names = vec![user_display_name];
team_names.extend(teams.iter().map(|team| team.name.as_str()));

let theme = ColorfulTheme {
let theme = DialoguerTheme {
active_item_style: Style::new().cyan().bold(),
active_item_prefix: Style::new().cyan().bold().apply_to(">".to_string()),
prompt_prefix: Style::new().dim().bold().apply_to("?".to_string()),
values_style: Style::new().cyan(),
..ColorfulTheme::default()
..DialoguerTheme::default()
};

let prompt = format!(
Expand Down Expand Up @@ -455,12 +455,12 @@ fn select_space<'a>(base: &CommandBase, spaces: &'a [Space]) -> Result<SelectedS
.map(|space| space.name.as_str())
.collect::<Vec<_>>();

let theme = ColorfulTheme {
let theme = DialoguerTheme {
active_item_style: Style::new().cyan().bold(),
active_item_prefix: Style::new().cyan().bold().apply_to(">".to_string()),
prompt_prefix: Style::new().dim().bold().apply_to("?".to_string()),
values_style: Style::new().cyan(),
..ColorfulTheme::default()
..DialoguerTheme::default()
};

let prompt = format!(
Expand Down
1 change: 1 addition & 0 deletions crates/turborepo-lib/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) mod login;
pub(crate) mod logout;
pub(crate) mod prune;
pub(crate) mod run;
pub(crate) mod scan;
pub(crate) mod telemetry;
pub(crate) mod unlink;

Expand Down
137 changes: 137 additions & 0 deletions crates/turborepo-lib/src/commands/scan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::{sync::LazyLock, time::Duration};

use console::{style, Style};
use futures::StreamExt;
use tokio_stream::StreamMap;
use turborepo_ui::*;

use super::CommandBase;
use crate::{diagnostics::*, DaemonPaths};

// can't use LazyCell since DialoguerTheme isn't Sync
static DIALOGUER_THEME: LazyLock<DialoguerTheme> = LazyLock::new(|| DialoguerTheme {
prompt_prefix: style(">>>".to_string()).bright().for_stderr(),
active_item_prefix: style(" ❯".to_string()).for_stderr().green(),
inactive_item_prefix: style(" ".to_string()).for_stderr(),
success_prefix: style(" ".to_string()).for_stderr(),
prompt_style: Style::new().for_stderr(),
..Default::default()
});

/// diagnostics run in parallel however to prevent messages from appearing too
/// quickly we introduce a minimum delay between each message
const INTER_MESSAGE_DELAY: Duration = Duration::from_millis(30);

/// Start a diagnostic session. This command will run a series of diagnostics to
/// help you identify potential performance bottlenecks in your monorepo.
///
/// Note: all lints happen in parallel. For the purposes of displaying output,
/// we demultiplex the output and display it in a single stream, meaning
/// to the user, it looks like the lints are running serially.
pub async fn run(base: CommandBase) -> bool {
let paths = DaemonPaths::from_repo_root(&base.repo_root);
let ui = base.ui;

println!("\n{}\n", ui.rainbow(">>> TURBO SCAN"));
println!(
"Turborepo does a lot of work behind the scenes to make your monorepo fast,
however, there are some things you can do to make it even faster. {}\n",
color!(ui, BOLD_GREEN, "Let's go!")
);

let mut all_events = StreamMap::new();

let d1 = Box::new(DaemonDiagnostic(paths.clone()));
let d2 = Box::new(LSPDiagnostic(paths));
let d3 = Box::new(GitDaemonDiagnostic);
let d5 = Box::new(UpdateDiagnostic(base.repo_root.clone()));
let d4 = Box::new(RemoteCacheDiagnostic::new(base));

let diags: Vec<Box<dyn Diagnostic>> = vec![d1, d2, d3, d4, d5];
let num_tasks: usize = diags.len();
for diag in diags {
let name = diag.name();
let (tx, rx) = DiagnosticChannel::new();
diag.execute(tx);
let wrapper = tokio_stream::wrappers::ReceiverStream::new(rx);
all_events.insert(name, wrapper);
}

let mut complete = 0;
let mut failed = 0;
let mut not_applicable = 0;

while let Some((diag, message)) = all_events.next().await {
use DiagnosticMessage::*;

let mut diag_events = all_events.remove(diag).expect("stream not found in map");

// the allowed opening message is 'started'
let human_name = match message {
Started(human_name) => human_name,
_other => {
panic!("this is a programming error, please report an issue");
}
};

let bar = start_spinner(&human_name);

while let Some(message) = diag_events.next().await {
match message {
Started(_) => {} // ignore duplicate start events
LogLine(line) => {
bar.println(color!(ui, GREY, " {}", line).to_string());
}
Request(prompt, mut options, chan) => {
let opt = bar.suspend(|| {
dialoguer::Select::with_theme(&*DIALOGUER_THEME)
.with_prompt(prompt)
.items(&options)
.default(0)
.interact()
.unwrap()
});

chan.send(options.swap_remove(opt)).unwrap();
}
Suspend(stopped, resume) => {
let bar = bar.clone();
let handle = tokio::task::spawn_blocking(move || {
bar.suspend(|| {
// sender is dropped, so we can unsuspend
resume.blocking_recv().ok();
});
});
stopped.send(()).ok(); // suspender doesn't need to be notified so failing ok
handle.await.expect("panic in suspend task");
}
Done(message) => {
bar.finish_with_message(color!(ui, BOLD_GREEN, "{}", message).to_string());
complete += 1;
}
Failed(message) => {
bar.finish_with_message(color!(ui, BOLD_RED, "{}", message).to_string());
failed += 1;
}
NotApplicable(name) => {
let n_a = color!(ui, GREY, "n/a").to_string();
let style = bar.style().tick_strings(&[&n_a, &n_a]);
bar.set_style(style);
bar.finish_with_message(color!(ui, BOLD_GREY, "{}", name).to_string());
not_applicable += 1;
}
};
if complete + not_applicable + failed == num_tasks {
break;
}
tokio::time::sleep(INTER_MESSAGE_DELAY).await;
}
}

if complete + not_applicable == num_tasks {
println!("\n\n{}", ui.rainbow(">>> FULL TURBO"));
true
} else {
false
}
}
Loading

0 comments on commit 1e25520

Please sign in to comment.