net: Optimize initial TLS connection (#44242)

By touching the crypto provider, we can force it to gather entropy.
On my linux VM, that moved ~60ms off the critical path (using aws-lc-rs,
but probably any other crypto provider would show similar behavior).
On my Linux workstation it was around ~30ms.

On Linux caching the rustls platform verifier cache optimizes another
50ms. On other platforms this will be cheaper, since
only on some systems all certificates are read. It might make sense to
explore
caching the whole tlsconfig (for websockets), since it looks like we are
just cloning
the same components and then constructing a new tlsconfig, which would
lead to
the same effective component. But that needs more investigation.


Testing: Doesn't change any visible behavior, covered by existing tests.

---------

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
This commit is contained in:
Jonathan Schwender
2026-04-22 09:39:59 +02:00
committed by GitHub
parent 09ef444d63
commit 97c08a6f95
3 changed files with 66 additions and 13 deletions

View File

@@ -74,8 +74,8 @@ servo-default-resources = { workspace = true, optional = true }
servo-tracing = { workspace = true }
servo-url = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true, optional = true }
time = { workspace = true }
tracing = { workspace = true, optional = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "fs"] }
tokio-rustls = { workspace = true }
tokio-stream = { workspace = true }

View File

@@ -4,7 +4,7 @@
use std::collections::hash_map::HashMap;
use std::convert::TryFrom;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use std::{fmt, io};
@@ -377,6 +377,7 @@ pub enum CACertificates<'de> {
/// FIXME: The `ignore_certificate_errors` argument ignores all certificate errors. This
/// is used when running the WPT tests, because rustls currently rejects the WPT certificiate.
/// See <https://github.com/servo/servo/issues/30080>
#[servo_tracing::instrument(skip_all)]
pub fn create_tls_config(
ca_certificates: CACertificates<'static>,
ignore_certificate_errors: bool,
@@ -387,6 +388,8 @@ pub fn create_tls_config(
ignore_certificate_errors,
override_manager,
);
// TODO: After <https://github.com/rustls/rustls-platform-verifier/pull/204> is merged,
// `dangerous` can be removed.
rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
@@ -405,6 +408,54 @@ where
}
}
static CRYPTO_PROVIDER_CACHE: LazyLock<Arc<CryptoProvider>> = LazyLock::new(|| {
CryptoProvider::get_default()
.cloned()
// The embedder should have initialized the default crypto provider before
// initializing servo, so this should never fail.
.unwrap_or_else(|| {
warn!("Default crypto provider not initialized before first access in connector.");
Arc::new(aws_lc_rs::default_provider())
})
});
/// A cache for the default rustls platform verifier.
///
/// Instantiating a new verifier can be expensive, since it can read through all certificates:
/// <https://github.com/rustls/rustls-platform-verifier/blob/996b1c903491641b17b3c9afb65d1352f6fc6b76/rustls-platform-verifier/src/verification/others.rs#L92>
static RUSTLS_PLATFORM_VERIFIER_CACHE: LazyLock<Arc<rustls_platform_verifier::Verifier>> =
LazyLock::new(|| {
Arc::new(
rustls_platform_verifier::Verifier::new(CRYPTO_PROVIDER_CACHE.clone())
.expect("Could not initialize platform certificate verifier"),
)
});
/// Prewarm the TLS stack to speed up the first connection
///
/// Currently, this force-seeds the crypto provider (from aws_lc_rs),
/// which on my system takes around 30-50ms according to samply, spent in
/// `tree_jitter_initialize_once`. If we don't call this function, then
/// the initialization will happen much later, on a tokio runtime thread.
#[inline]
pub fn prewarm_tls() {
#[servo_tracing::instrument]
fn prewarm_tls_impl() {
let mut sink = [0u8; 32];
// The first access can be slow, if the provider needs to gather entropy.
let _ = CRYPTO_PROVIDER_CACHE.secure_random.fill(&mut sink);
// Note: We don't need to explicitly force initialize RUSTLS_PLATFORM_VERIFIER_CACHE,
// since the resource manager thread will do that during startup.
}
if let Err(error) = std::thread::Builder::new()
.name("Net-TLS-prewarm".into())
.spawn(prewarm_tls_impl)
{
warn!("Failed to spawn thread to prewarm TLS: {error:?}");
}
}
#[derive(Debug)]
struct CertificateVerificationOverrideVerifier {
main_verifier: Arc<dyn ServerCertVerifier>,
@@ -428,25 +479,25 @@ impl CertificateVerificationOverrideVerifier {
// on Android.
let use_webpki_roots = cfg!(target_os = "android") || pref!(network_use_webpki_roots);
let main_verifier = if !use_webpki_roots {
let crypto_provider = CryptoProvider::get_default()
.unwrap_or(&Arc::new(aws_lc_rs::default_provider()))
.clone();
let verifier = match ca_certficates {
CACertificates::Default => rustls_platform_verifier::Verifier::new(crypto_provider),
CACertificates::Default => RUSTLS_PLATFORM_VERIFIER_CACHE.clone(),
// Android doesn't support `Verifier::new_with_extra_roots`, but currently Android
// never uses the platform verifier at all.
CACertificates::Override(_certificates) => {
#[cfg(target_os = "android")]
unreachable!("Android should always use the WebPKI verifier.");
#[cfg(not(target_os = "android"))]
rustls_platform_verifier::Verifier::new_with_extra_roots(
_certificates,
crypto_provider,
)
{
let verifier = rustls_platform_verifier::Verifier::new_with_extra_roots(
_certificates,
CRYPTO_PROVIDER_CACHE.clone(),
)
.expect("Could not initialize platform certificate verifier");
Arc::new(verifier)
}
},
}
.expect("Could not initialize platform certificate verifier");
Arc::new(verifier) as Arc<dyn ServerCertVerifier>
};
verifier as Arc<dyn ServerCertVerifier>
} else {
let mut root_store =
rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

View File

@@ -948,6 +948,8 @@ impl Servo {
private_storage_threads.clone(),
);
net::connector::prewarm_tls();
if opts::get().multiprocess {
prefs::add_observer(Box::new(constellation_proxy.clone()));
}