Skip to content

Commit ab52ef1

Browse files
committed
perf: Share single reqwest::Client to eliminate redundant TLS initialization
Previously, three separate reqwest::Client instances were built during startup (API client, cache client, telemetry client), each performing independent TLS initialization (~100-150ms each on macOS). This was the single largest contributor to startup latency. Now a single reqwest::Client is built once in cli::run() and shared across all consumers via new_with_client() constructors. Per-request timeouts replace per-client timeouts, preserving the existing timeout semantics (API timeout for regular calls, upload timeout for cache uploads). Includes regression tests covering all client operations: get_user, get_teams, get_caching_status, put/fetch/exists artifact, telemetry recording, and timeout configurations.
1 parent 34e1e75 commit ab52ef1

File tree

10 files changed

+392
-74
lines changed

10 files changed

+392
-74
lines changed

crates/turborepo-api-client/src/lib.rs

Lines changed: 332 additions & 47 deletions
Large diffs are not rendered by default.

crates/turborepo-lib/src/cli/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ pub async fn print_potential_tasks(
9191
let handler = SignalHandler::new(signal);
9292
let color_config = base.color_config;
9393

94-
let run_builder = RunBuilder::new(base)?;
94+
let run_builder = RunBuilder::new(base, None)?;
9595
let run = run_builder.build(&handler, telemetry).await?;
9696
let potential_tasks = run.get_potential_tasks()?;
9797

crates/turborepo-lib/src/cli/mod.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub use error::Error;
77
use serde::Serialize;
88
use tracing::{debug, error, log::warn};
99
use turbopath::AbsoluteSystemPathBuf;
10-
use turborepo_api_client::AnonAPIClient;
10+
use turborepo_api_client::{APIClient, AnonAPIClient};
1111
use turborepo_repository::inference::{RepoMode, RepoState};
1212
use turborepo_telemetry::{
1313
events::{command::CommandEventBuilder, generic::GenericEventBuilder, EventBuilder, EventType},
@@ -1140,25 +1140,22 @@ impl RunArgs {
11401140

11411141
#[tracing::instrument(skip_all)]
11421142
fn initialize_telemetry_client(
1143+
http_client: &reqwest::Client,
11431144
color_config: ColorConfig,
11441145
version: &str,
11451146
) -> Option<TelemetryHandle> {
1146-
let mut telemetry_handle: Option<TelemetryHandle> = None;
1147-
match AnonAPIClient::new("https://telemetry.vercel.com", 250, version) {
1148-
Ok(anonymous_api_client) => {
1149-
let handle = init_telemetry(anonymous_api_client, color_config);
1150-
match handle {
1151-
Ok(h) => telemetry_handle = Some(h),
1152-
Err(error) => {
1153-
debug!("failed to start telemetry: {:?}", error)
1154-
}
1155-
}
1156-
}
1147+
let anonymous_api_client = AnonAPIClient::new_with_client(
1148+
http_client.clone(),
1149+
"https://telemetry.vercel.com",
1150+
version,
1151+
);
1152+
match init_telemetry(anonymous_api_client, color_config) {
1153+
Ok(h) => Some(h),
11571154
Err(error) => {
1158-
debug!("Failed to create AnonAPIClient: {:?}", error);
1155+
debug!("failed to start telemetry: {:?}", error);
1156+
None
11591157
}
11601158
}
1161-
telemetry_handle
11621159
}
11631160

11641161
#[derive(PartialEq)]
@@ -1305,8 +1302,13 @@ pub async fn run(
13051302
let mut cli_args = Args::new(env::args_os().collect());
13061303
let version = get_version();
13071304

1305+
// Build a single HTTP client to share across telemetry, API, and cache
1306+
// operations. This avoids redundant TLS initialization (~150ms savings).
1307+
let http_client = APIClient::build_http_client(None)
1308+
.expect("Failed to create HTTP client: TLS initialization failed");
1309+
13081310
// track telemetry handle to close at the end of the run
1309-
let telemetry_handle = initialize_telemetry_client(color_config, version);
1311+
let telemetry_handle = initialize_telemetry_client(&http_client, color_config, version);
13101312

13111313
if should_print_version() {
13121314
eprintln!("{}", GREY.apply_to(format!("• turbo {}", get_version())));
@@ -1599,7 +1601,7 @@ pub async fn run(
15991601
}
16001602

16011603
run_args.track(&event);
1602-
let exit_code = run::run(base, event).await.inspect(|code| {
1604+
let exit_code = run::run(base, event, &http_client).await.inspect(|code| {
16031605
if *code != 0 {
16041606
error!("run failed: command exited ({code})");
16051607
}

crates/turborepo-lib/src/commands/boundaries.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub async fn run(
1818
let signal = get_signal()?;
1919
let handler = SignalHandler::new(signal);
2020

21-
let run = RunBuilder::new(base)?
21+
let run = RunBuilder::new(base, None)?
2222
.do_not_validate_engine()
2323
.build(&handler, telemetry)
2424
.await?;

crates/turborepo-lib/src/commands/ls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ pub async fn run(
124124
let signal = get_signal()?;
125125
let handler = SignalHandler::new(signal);
126126

127-
let run_builder = RunBuilder::new(base)?;
127+
let run_builder = RunBuilder::new(base, None)?;
128128
let run = run_builder.build(&handler, telemetry).await?;
129129

130130
if packages.is_empty() {

crates/turborepo-lib/src/commands/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@ impl CommandBase {
142142
.map_err(ConfigError::ApiClient)
143143
}
144144

145+
/// Creates an API client using a pre-built HTTP client to avoid
146+
/// redundant TLS initialization.
147+
pub fn api_client_with_http(&self, http_client: &reqwest::Client) -> APIClient {
148+
let timeout = self.opts.api_client_opts.timeout;
149+
let upload_timeout = self.opts.api_client_opts.upload_timeout;
150+
151+
APIClient::new_with_client(
152+
http_client.clone(),
153+
&self.opts.api_client_opts.api_url,
154+
if timeout > 0 {
155+
Some(Duration::from_secs(timeout))
156+
} else {
157+
None
158+
},
159+
if upload_timeout > 0 {
160+
Some(Duration::from_secs(upload_timeout))
161+
} else {
162+
None
163+
},
164+
self.version,
165+
self.opts.api_client_opts.preflight,
166+
)
167+
}
168+
145169
/// Current working directory for the turbo command
146170
pub fn cwd(&self) -> &AbsoluteSystemPath {
147171
// Earlier in execution

crates/turborepo-lib/src/commands/query.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pub async fn run(
164164
let signal = get_signal()?;
165165
let handler = SignalHandler::new(signal);
166166

167-
let run_builder = RunBuilder::new(base)?
167+
let run_builder = RunBuilder::new(base, None)?
168168
.add_all_tasks()
169169
.do_not_validate_engine();
170170
let run = run_builder.build(&handler, telemetry).await?;

crates/turborepo-lib/src/commands/run.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ use turborepo_ui::sender::UISender;
88
use crate::{commands::CommandBase, run, run::builder::RunBuilder};
99

1010
#[tracing::instrument(skip_all)]
11-
pub async fn run(base: CommandBase, telemetry: CommandEventBuilder) -> Result<i32, run::Error> {
11+
pub async fn run(
12+
base: CommandBase,
13+
telemetry: CommandEventBuilder,
14+
http_client: &reqwest::Client,
15+
) -> Result<i32, run::Error> {
1216
let signal = get_signal()?;
1317
let handler = SignalHandler::new(signal);
1418

15-
let run_builder = RunBuilder::new(base)?;
19+
let run_builder = RunBuilder::new(base, Some(http_client))?;
1620

1721
let run_fut = async {
1822
let (analytics_sender, analytics_handle) = run_builder.start_analytics();

crates/turborepo-lib/src/run/builder.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ pub struct RunBuilder {
6464

6565
impl RunBuilder {
6666
#[tracing::instrument(skip_all)]
67-
pub fn new(base: CommandBase) -> Result<Self, Error> {
68-
let api_client = base.api_client()?;
67+
pub fn new(base: CommandBase, http_client: Option<&reqwest::Client>) -> Result<Self, Error> {
68+
let api_client = match http_client {
69+
Some(client) => base.api_client_with_http(client),
70+
None => base.api_client()?,
71+
};
6972

7073
let opts = base.opts();
7174
let api_auth = base.api_auth()?;

crates/turborepo-lib/src/run/watch.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl WatchClient {
123123

124124
let new_base = base.clone();
125125
let run = Arc::new(
126-
RunBuilder::new(new_base)?
126+
RunBuilder::new(new_base, None)?
127127
.build(&handler, telemetry.clone())
128128
.await?,
129129
);
@@ -355,7 +355,7 @@ impl WatchClient {
355355
let signal_handler = self.handler.clone();
356356
let telemetry = self.telemetry.clone();
357357

358-
let run = RunBuilder::new(new_base)?
358+
let run = RunBuilder::new(new_base, None)?
359359
.with_entrypoint_packages(packages)
360360
.hide_prelude()
361361
.build(&signal_handler, telemetry)
@@ -389,7 +389,7 @@ impl WatchClient {
389389
);
390390

391391
// rebuild run struct
392-
self.run = RunBuilder::new(base.clone())?
392+
self.run = RunBuilder::new(base.clone(), None)?
393393
.hide_prelude()
394394
.build(&self.handler, self.telemetry.clone())
395395
.await?

0 commit comments

Comments
 (0)