Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
960dbbf
fix: Resolve Windows compilation error in binary_search type coercion
anthonyshew Feb 21, 2026
d996522
perf: Optimize engine builder, task visitor, and untracked file disco…
anthonyshew Feb 21, 2026
d5dfd6e
perf: Parallelize turbo run pre-execution hot path
anthonyshew Feb 22, 2026
af9d463
Merge branch 'main' of https://github.com/vercel/turborepo into perf/…
anthonyshew Feb 22, 2026
bf36bfa
fix: Remove dead code from Dependencies::new and DependencySplitter::…
anthonyshew Feb 22, 2026
b13977c
Merge branch 'main' of https://github.com/vercel/turborepo into perf/…
anthonyshew Feb 22, 2026
493073e
chore: Add tracing spans to previously uninstrumented hot path functions
anthonyshew Feb 22, 2026
0e826b1
Merge branch 'main' of https://github.com/vercel/turborepo into chore…
anthonyshew Feb 22, 2026
0739ca5
Merge branch 'main' of https://github.com/vercel/turborepo into chore…
anthonyshew Feb 22, 2026
c06149c
perf: Use Arc<str> for task dependency hashes to avoid heap clones
anthonyshew Feb 22, 2026
05de926
perf: Propagate Arc<str> through HashTrackerInfo trait and SharedTask…
anthonyshew Feb 22, 2026
33b8a84
fix: Explicitly enable serde/rc for Arc<str> serialization
anthonyshew Feb 22, 2026
8e216e4
Merge branch 'main' of https://github.com/vercel/turborepo into perf/…
anthonyshew Feb 22, 2026
7255ce8
perf: Add tracing spans across the entire turbo run startup path
anthonyshew Feb 23, 2026
c5972e1
fix: Use .instrument() instead of .entered() across await point
anthonyshew Feb 23, 2026
8a5823d
perf: Defer TLS initialization to a background thread
anthonyshew Feb 23, 2026
7dcebb7
perf: Defer TLS initialization to a background thread
anthonyshew Feb 23, 2026
a82c4a3
Merge remote-tracking branch 'refs/remotes/origin/perf/defer-tls-init…
anthonyshew Feb 23, 2026
e0e0dde
fix: Replace panicking expects with error propagation in deferred TLS…
anthonyshew Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/turborepo-api-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum Error {
TooManyFailures(#[from] Box<reqwest::Error>),
#[error("Unable to set up TLS.")]
TlsError(#[source] reqwest::Error),
#[error("HTTP client initialization was cancelled (runtime shutting down)")]
HttpClientCancelled,
#[error("Error parsing header: {0}")]
InvalidHeader(#[from] ToStrError),
#[error("Error parsing '{url}' as URL: {err}")]
Expand Down
71 changes: 69 additions & 2 deletions crates/turborepo-api-client/src/telemetry.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::future::Future;
use std::{future::Future, sync::Arc};

use reqwest::Method;
use tokio::sync::OnceCell;
use turborepo_vercel_api::telemetry::TelemetryEvent;

use crate::{AnonAPIClient, Error, retry};
use crate::{APIClient, AnonAPIClient, Error, build_user_agent, retry};

const TELEMETRY_ENDPOINT: &str = "/api/turborepo/v1/events";

Expand Down Expand Up @@ -41,3 +42,69 @@ impl TelemetryClient for AnonAPIClient {
Ok(())
}
}

/// A telemetry client backed by an HTTP client that initializes on a
/// background thread. TLS initialization (~100ms) starts as early as
/// possible via `spawn_blocking`; this client shares the `OnceCell` that
/// the background task writes to. By the time telemetry flushes its
/// first batch, TLS init has almost certainly already completed.
#[derive(Clone)]
pub struct DeferredTelemetryClient {
http_client: Arc<OnceCell<reqwest::Client>>,
base_url: String,
user_agent: String,
}

impl DeferredTelemetryClient {
pub fn new(
http_client: Arc<OnceCell<reqwest::Client>>,
base_url: impl Into<String>,
version: &str,
) -> Self {
Self {
http_client,
base_url: base_url.into(),
user_agent: build_user_agent(version),
}
}
}

impl TelemetryClient for DeferredTelemetryClient {
async fn record_telemetry(
&self,
events: Vec<TelemetryEvent>,
telemetry_id: &str,
session_id: &str,
) -> Result<(), Error> {
// Fast path: background TLS init already completed.
// Slow path: initialize inline, but if the runtime is shutting down
// the spawn_blocking task will be cancelled — return an error instead
// of panicking. Telemetry is never worth crashing over.
let maybe_client;
let client = match self.http_client.get() {
Some(client) => client,
None => {
maybe_client = tokio::task::spawn_blocking(|| APIClient::build_http_client(None))
.await
.map_err(|_| Error::HttpClientCancelled)??;
&maybe_client
}
};

let url = format!("{}{}", self.base_url, TELEMETRY_ENDPOINT);
let telemetry_request = client
.request(Method::POST, url)
.header("User-Agent", self.user_agent.clone())
.header("Content-Type", "application/json")
.header("x-turbo-telemetry-id", telemetry_id)
.header("x-turbo-session-id", session_id)
.json(&events);

retry::make_retryable_request(telemetry_request, retry::RetryStrategy::Timeout)
.await?
.into_response()
.error_for_status()?;

Ok(())
}
}
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/cli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub async fn print_potential_tasks(
let color_config = base.color_config;

let run_builder = RunBuilder::new(base, None)?;
let run = run_builder.build(&handler, telemetry).await?;
let (run, _analytics) = run_builder.build(&handler, telemetry).await?;
let potential_tasks = run.get_potential_tasks()?;

println!("No tasks provided, here are some potential ones\n",);
Expand Down
65 changes: 50 additions & 15 deletions crates/turborepo-lib/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,25 @@ fn initialize_telemetry_client(
}
}

fn initialize_deferred_telemetry_client(
http_client_cell: std::sync::Arc<tokio::sync::OnceCell<reqwest::Client>>,
color_config: ColorConfig,
version: &str,
) -> Option<TelemetryHandle> {
let deferred_client = turborepo_api_client::telemetry::DeferredTelemetryClient::new(
http_client_cell,
"https://telemetry.vercel.com",
version,
);
match init_telemetry(deferred_client, color_config) {
Ok(h) => Some(h),
Err(error) => {
debug!("failed to start telemetry: {:?}", error);
None
}
}
}

#[derive(PartialEq)]
enum PrintVersionState {
Enabled,
Expand Down Expand Up @@ -1300,21 +1319,38 @@ pub async fn run(
) -> Result<i32, Error> {
let _cli_run_span = tracing::info_span!("cli_run").entered();

// Spawn TLS initialization on a background thread immediately.
// This takes ~100ms (loading root certificates, TLS backend init)
// and runs fully in the background. Neither telemetry nor the run
// path block on it — the HTTP client is resolved lazily when the
// first network request is actually needed.
let http_client_cell = std::sync::Arc::new(tokio::sync::OnceCell::<reqwest::Client>::new());
{
let cell = http_client_cell.clone();
tokio::task::spawn(async move {
if let Ok(Ok(client)) = tokio::task::spawn_blocking(|| {
let _span = tracing::info_span!("http_client_init").entered();
APIClient::build_http_client(None)
})
.await
{
cell.set(client).ok();
}
});
}

let mut cli_args = {
let _span = tracing::info_span!("cli_arg_parsing").entered();
Args::new(env::args_os().collect())
};
let version = get_version();

let http_client = {
let _span = tracing::info_span!("http_client_init").entered();
APIClient::build_http_client(None)
.expect("Failed to create HTTP client: TLS initialization failed")
};

// Initialize telemetry immediately with a deferred HTTP client.
// Events are queued to a channel from the start; the actual HTTP
// client is only resolved when the worker flushes its first batch.
let telemetry_handle = {
let _span = tracing::info_span!("telemetry_init").entered();
initialize_telemetry_client(&http_client, color_config, version)
initialize_deferred_telemetry_client(http_client_cell.clone(), color_config, version)
};

if should_print_version() {
Expand All @@ -1326,9 +1362,6 @@ pub async fn run(
// Set some run flags if we have the data and are executing a Run
set_run_flags(&mut command, &repo_state, &cli_args)?;

// TODO: make better use of RepoState, here and below. We've already inferred
// the repo root, we don't need to calculate it again, along with package
// manager inference.
let cwd = repo_state
.as_ref()
.map(|state| state.root.as_path())
Expand Down Expand Up @@ -1611,11 +1644,13 @@ pub async fn run(
}

run_args.track(&event);
let exit_code = run::run(base, event, &http_client).await.inspect(|code| {
if *code != 0 {
error!("run failed: command exited ({code})");
}
})?;
let exit_code = run::run(base, event, http_client_cell)
.await
.inspect(|code| {
if *code != 0 {
error!("run failed: command exited ({code})");
}
})?;

// Chrome tracing is enabled early in shim::run(). Here we just
// flush and generate the markdown summary.
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/commands/boundaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub async fn run(
let signal = get_signal()?;
let handler = SignalHandler::new(signal);

let run = RunBuilder::new(base, None)?
let (run, _analytics) = RunBuilder::new(base, None)?
.do_not_validate_engine()
.build(&handler, telemetry)
.await?;
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/commands/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ pub async fn run(
let handler = SignalHandler::new(signal);

let run_builder = RunBuilder::new(base, None)?;
let run = run_builder.build(&handler, telemetry).await?;
let (run, _analytics) = run_builder.build(&handler, telemetry).await?;

if packages.is_empty() {
RepositoryDetails::new(&run).print(output)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/commands/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ pub async fn run(
let run_builder = RunBuilder::new(base, None)?
.add_all_tasks()
.do_not_validate_engine();
let run = run_builder.build(&handler, telemetry).await?;
let (run, _analytics) = run_builder.build(&handler, telemetry).await?;
let query = query.as_deref().or(include_schema.then_some(SCHEMA_QUERY));
if let Some(query) = query {
let trimmed_query = query.trim();
Expand Down
15 changes: 6 additions & 9 deletions crates/turborepo-lib/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,21 @@ use crate::{commands::CommandBase, run, run::builder::RunBuilder};
pub async fn run(
base: CommandBase,
telemetry: CommandEventBuilder,
http_client: &reqwest::Client,
http_client_cell: Arc<tokio::sync::OnceCell<reqwest::Client>>,
) -> Result<i32, run::Error> {
let signal = get_signal()?;
let handler = SignalHandler::new(signal);

let run_builder = {
let _span = tracing::info_span!("run_builder_new").entered();
RunBuilder::new(base, Some(http_client))?
RunBuilder::new(base, Some(http_client_cell))?
};

let run_fut = async {
let (analytics_sender, analytics_handle) = run_builder.start_analytics();
let run = Arc::new(
run_builder
.with_analytics_sender(analytics_sender)
.build(&handler, telemetry)
.await?,
);
let (run, analytics_handle) = {
let (run, analytics_handle) = run_builder.build(&handler, telemetry).await?;
(Arc::new(run), analytics_handle)
};

let (sender, handle) = {
let _span = tracing::info_span!("start_ui").entered();
Expand Down
Loading
Loading