| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/preloading/preconnect/preconnect_manager_impl.h" |
| |
| #include <utility> |
| |
| #include "base/containers/adapters.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/types/optional_util.h" |
| #include "content/browser/preloading/proxy_lookup_client_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/preconnect_request.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/content_features.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "services/network/public/mojom/connection_change_observer_client.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| |
| namespace content { |
| |
| const bool kAllowCredentialsOnPreconnectByDefault = true; |
| |
| std::unique_ptr<PreconnectManager> PreconnectManager::Create( |
| base::WeakPtr<PreconnectManager::Delegate> delegate, |
| content::BrowserContext* browser_context) { |
| return std::make_unique<PreconnectManagerImpl>(std::move(delegate), |
| browser_context); |
| } |
| |
| PreconnectedRequestStats::PreconnectedRequestStats(const url::Origin& origin, |
| bool was_preconnected) |
| : origin(origin), was_preconnected(was_preconnected) {} |
| |
| PreconnectedRequestStats::PreconnectedRequestStats( |
| const PreconnectedRequestStats& other) = default; |
| PreconnectedRequestStats::~PreconnectedRequestStats() = default; |
| |
| PreconnectStats::PreconnectStats(const GURL& url) |
| : url(url), start_time(base::TimeTicks::Now()) {} |
| PreconnectStats::~PreconnectStats() = default; |
| |
| PreresolveInfo::PreresolveInfo(const GURL& url, size_t count) |
| : url(url), |
| queued_count(count), |
| inflight_count(0), |
| was_canceled(false), |
| stats(std::make_unique<PreconnectStats>(url)) {} |
| |
| PreresolveInfo::~PreresolveInfo() = default; |
| |
| PreresolveJob::PreresolveJob( |
| const GURL& url, |
| int num_sockets, |
| bool allow_credentials, |
| net::NetworkAnonymizationKey network_anonymization_key, |
| net::NetworkTrafficAnnotationTag traffic_annotation_tag, |
| std::optional<content::StoragePartitionConfig> storage_partition_config, |
| std::optional<net::ConnectionKeepAliveConfig> keepalive_config, |
| mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient> |
| connection_change_observer_client, |
| PreresolveInfo* info) |
| : url(url), |
| num_sockets(num_sockets), |
| allow_credentials(allow_credentials), |
| network_anonymization_key(std::move(network_anonymization_key)), |
| traffic_annotation_tag(std::move(traffic_annotation_tag)), |
| storage_partition_config(std::move(storage_partition_config)), |
| keepalive_config(std::move(keepalive_config)), |
| connection_change_observer_client( |
| std::move(connection_change_observer_client)), |
| info(info), |
| creation_time(base::TimeTicks::Now()) { |
| CHECK_GE(num_sockets, 0); |
| |
| CHECK(!this->network_anonymization_key.IsEmpty() || |
| !net::NetworkAnonymizationKey::IsPartitioningEnabled()); |
| } |
| |
| PreresolveJob::PreresolveJob( |
| content::PreconnectRequest preconnect_request, |
| PreresolveInfo* info, |
| net::NetworkTrafficAnnotationTag traffic_annotation_tag) |
| : PreresolveJob(preconnect_request.origin.GetURL(), |
| preconnect_request.num_sockets, |
| preconnect_request.allow_credentials, |
| std::move(preconnect_request.network_anonymization_key), |
| traffic_annotation_tag, |
| /*storage_partition_config=*/std::nullopt, |
| std::nullopt, |
| mojo::NullRemote(), |
| info) {} |
| |
| PreresolveJob::PreresolveJob(PreresolveJob&& other) = default; |
| PreresolveJob::~PreresolveJob() = default; |
| |
| PreconnectManagerImpl::PreconnectManagerImpl( |
| base::WeakPtr<Delegate> delegate, |
| content::BrowserContext* browser_context) |
| : delegate_(std::move(delegate)), |
| browser_context_(browser_context), |
| inflight_preresolves_count_(0) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(browser_context_); |
| } |
| |
| PreconnectManagerImpl::~PreconnectManagerImpl() = default; |
| |
| base::WeakPtr<PreconnectManager> PreconnectManagerImpl::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void PreconnectManagerImpl::SetNetworkContextForTesting( |
| network::mojom::NetworkContext* network_context) { |
| network_context_ = network_context; |
| } |
| |
| void PreconnectManagerImpl::SetObserverForTesting(Observer* observer) { |
| observer_ = observer; |
| } |
| |
| void PreconnectManagerImpl::Start( |
| const GURL& url, |
| std::vector<content::PreconnectRequest> requests, |
| net::NetworkTrafficAnnotationTag traffic_annotation) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!delegate_ || !delegate_->IsPreconnectEnabled()) { |
| return; |
| } |
| if (!url.SchemeIsHTTPOrHTTPS()) { |
| return; |
| } |
| PreresolveInfo* info; |
| if (preresolve_info_.find(url) == preresolve_info_.end()) { |
| auto iterator_and_whether_inserted = preresolve_info_.emplace( |
| url, std::make_unique<PreresolveInfo>(url, requests.size())); |
| info = iterator_and_whether_inserted.first->second.get(); |
| } else { |
| info = preresolve_info_.find(url)->second.get(); |
| info->queued_count += requests.size(); |
| } |
| |
| for (auto& request : requests) { |
| PreresolveJobId job_id = |
| preresolve_jobs_.Add(std::make_unique<PreresolveJob>( |
| std::move(request), info, traffic_annotation)); |
| queued_jobs_.push_back(job_id); |
| } |
| |
| TryToLaunchPreresolveJobs(); |
| } |
| |
| void PreconnectManagerImpl::StartPreresolveHost( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| const content::StoragePartitionConfig* storage_partition_config) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!delegate_ || !delegate_->IsPreconnectEnabled()) { |
| return; |
| } |
| if (!url.SchemeIsHTTPOrHTTPS()) { |
| return; |
| } |
| PreresolveJobId job_id = preresolve_jobs_.Add(std::make_unique<PreresolveJob>( |
| url.DeprecatedGetOriginAsURL(), 0, kAllowCredentialsOnPreconnectByDefault, |
| network_anonymization_key, traffic_annotation, |
| base::OptionalFromPtr(storage_partition_config), std::nullopt, |
| mojo::NullRemote(), nullptr)); |
| queued_jobs_.push_front(job_id); |
| |
| TryToLaunchPreresolveJobs(); |
| } |
| |
| void PreconnectManagerImpl::StartPreresolveHosts( |
| const std::vector<GURL>& urls, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| const content::StoragePartitionConfig* storage_partition_config) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!delegate_ || !delegate_->IsPreconnectEnabled()) { |
| return; |
| } |
| // Push jobs in front of the queue due to higher priority. |
| for (const GURL& url : base::Reversed(urls)) { |
| if (!url.SchemeIsHTTPOrHTTPS()) { |
| continue; |
| } |
| PreresolveJobId job_id = |
| preresolve_jobs_.Add(std::make_unique<PreresolveJob>( |
| url.DeprecatedGetOriginAsURL(), 0, |
| kAllowCredentialsOnPreconnectByDefault, network_anonymization_key, |
| traffic_annotation, base::OptionalFromPtr(storage_partition_config), |
| std::nullopt, mojo::NullRemote(), nullptr)); |
| queued_jobs_.push_front(job_id); |
| } |
| |
| TryToLaunchPreresolveJobs(); |
| } |
| |
| void PreconnectManagerImpl::StartPreconnectUrl( |
| const GURL& url, |
| bool allow_credentials, |
| net::NetworkAnonymizationKey network_anonymization_key, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| const content::StoragePartitionConfig* storage_partition_config, |
| std::optional<net::ConnectionKeepAliveConfig> keepalive_config, |
| mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient> |
| connection_change_observer_client) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!delegate_ || !delegate_->IsPreconnectEnabled()) { |
| return; |
| } |
| if (!url.SchemeIsHTTPOrHTTPS()) { |
| return; |
| } |
| PreresolveJobId job_id = preresolve_jobs_.Add(std::make_unique<PreresolveJob>( |
| url.DeprecatedGetOriginAsURL(), 1, allow_credentials, |
| std::move(network_anonymization_key), traffic_annotation, |
| base::OptionalFromPtr(storage_partition_config), |
| std::move(keepalive_config), std::move(connection_change_observer_client), |
| nullptr)); |
| queued_jobs_.push_front(job_id); |
| |
| TryToLaunchPreresolveJobs(); |
| } |
| |
| void PreconnectManagerImpl::Stop(const GURL& url) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| auto it = preresolve_info_.find(url); |
| if (it == preresolve_info_.end()) { |
| return; |
| } |
| |
| it->second->was_canceled = true; |
| } |
| |
| void PreconnectManagerImpl::PreconnectUrl( |
| const GURL& url, |
| int num_sockets, |
| bool allow_credentials, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| const net::NetworkTrafficAnnotationTag& traffic_annotation, |
| const content::StoragePartitionConfig* storage_partition_config, |
| std::optional<net::ConnectionKeepAliveConfig> keepalive_config, |
| mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient> |
| connection_change_observer_client) const { |
| DCHECK(url.DeprecatedGetOriginAsURL() == url); |
| DCHECK(url.SchemeIsHTTPOrHTTPS()); |
| if (observer_) { |
| observer_->OnPreconnectUrl(url, num_sockets, allow_credentials); |
| } |
| |
| auto* network_context = GetNetworkContext(storage_partition_config); |
| |
| if (num_sockets > 1 && |
| base::FeatureList::IsEnabled( |
| features::kLoadingPredictorLimitPreconnectSocketCount)) { |
| // Adjust the number of socket here because LoadingPredictor is the only |
| // call site that sets `num_sockets` to a non-one value. |
| num_sockets = 1; |
| } |
| |
| // TODO(crbug.com/406022435): pass the actual `keepalive_config` from the |
| // caller. |
| network_context->PreconnectSockets( |
| num_sockets, url, |
| allow_credentials ? network::mojom::CredentialsMode::kInclude |
| : network::mojom::CredentialsMode::kOmit, |
| network_anonymization_key, |
| net::MutableNetworkTrafficAnnotationTag(traffic_annotation), |
| std::move(keepalive_config), |
| std::move(connection_change_observer_client)); |
| } |
| |
| std::unique_ptr<ResolveHostClientImpl> PreconnectManagerImpl::PreresolveUrl( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| const content::StoragePartitionConfig* storage_partition_config, |
| ResolveHostCallback callback) const { |
| DCHECK(url.DeprecatedGetOriginAsURL() == url); |
| DCHECK(url.SchemeIsHTTPOrHTTPS()); |
| |
| auto* network_context = GetNetworkContext(storage_partition_config); |
| |
| return std::make_unique<ResolveHostClientImpl>( |
| url, network_anonymization_key, std::move(callback), network_context); |
| } |
| |
| std::unique_ptr<ProxyLookupClientImpl> PreconnectManagerImpl::LookupProxyForUrl( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| const content::StoragePartitionConfig* storage_partition_config, |
| ProxyLookupClientImpl::ProxyLookupCallback callback) const { |
| DCHECK(url.DeprecatedGetOriginAsURL() == url); |
| DCHECK(url.SchemeIsHTTPOrHTTPS()); |
| |
| auto* network_context = GetNetworkContext(storage_partition_config); |
| |
| return std::make_unique<ProxyLookupClientImpl>( |
| url, network_anonymization_key, std::move(callback), network_context); |
| } |
| |
| void PreconnectManagerImpl::TryToLaunchPreresolveJobs() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // We assume that the number of jobs in the queue will be relatively small at |
| // any given time. We can revisit this as needed. |
| UMA_HISTOGRAM_COUNTS_100("Navigation.Preconnect.PreresolveJobQueueLength", |
| queued_jobs_.size()); |
| |
| while (!queued_jobs_.empty() && |
| inflight_preresolves_count_ < kMaxInflightPreresolves) { |
| auto job_id = queued_jobs_.front(); |
| queued_jobs_.pop_front(); |
| PreresolveJob* job = preresolve_jobs_.Lookup(job_id); |
| DCHECK(job); |
| |
| // Note: PreresolveJobs are put into |queued_jobs_| immediately on creation, |
| // so their creation time is also the time at which they started queueing. |
| UMA_HISTOGRAM_TIMES("Navigation.Preconnect.PreresolveJobQueueingTime", |
| base::TimeTicks::Now() - job->creation_time); |
| |
| PreresolveInfo* info = job->info; |
| |
| if (!(info && info->was_canceled)) { |
| // This is used to avoid issuing DNS requests when a fixed proxy |
| // configuration is in place, which improves efficiency, and is also |
| // important if the unproxied DNS may contain incorrect entries. |
| job->proxy_lookup_client = LookupProxyForUrl( |
| job->url, job->network_anonymization_key, |
| base::OptionalToPtr(job->storage_partition_config), |
| base::BindOnce(&PreconnectManagerImpl::OnProxyLookupFinished, |
| weak_factory_.GetWeakPtr(), job_id)); |
| if (info) { |
| ++info->inflight_count; |
| if (delegate_) { |
| delegate_->PreconnectInitiated(info->url, job->url); |
| } |
| } |
| ++inflight_preresolves_count_; |
| } else { |
| preresolve_jobs_.Remove(job_id); |
| } |
| |
| if (info) { |
| DCHECK_LE(1u, info->queued_count); |
| --info->queued_count; |
| if (info->is_done()) { |
| AllPreresolvesForUrlFinished(info); |
| } |
| } |
| } |
| } |
| |
| void PreconnectManagerImpl::OnPreresolveFinished(PreresolveJobId job_id, |
| bool success) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PreresolveJob* job = preresolve_jobs_.Lookup(job_id); |
| DCHECK(job); |
| |
| if (observer_) { |
| observer_->OnPreresolveFinished(job->url, job->network_anonymization_key, |
| job->connection_change_observer_client, |
| success); |
| } |
| |
| job->resolve_host_client = nullptr; |
| FinishPreresolveJob(job_id, success); |
| } |
| |
| void PreconnectManagerImpl::OnProxyLookupFinished(PreresolveJobId job_id, |
| bool success) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PreresolveJob* job = preresolve_jobs_.Lookup(job_id); |
| DCHECK(job); |
| |
| if (observer_) { |
| observer_->OnProxyLookupFinished(job->url, job->network_anonymization_key, |
| success); |
| } |
| |
| job->proxy_lookup_client = nullptr; |
| if (success) { |
| FinishPreresolveJob(job_id, success); |
| } else { |
| job->resolve_host_client = PreresolveUrl( |
| job->url, job->network_anonymization_key, |
| base::OptionalToPtr(job->storage_partition_config), |
| base::BindOnce(&PreconnectManagerImpl::OnPreresolveFinished, |
| weak_factory_.GetWeakPtr(), job_id)); |
| } |
| } |
| |
| void PreconnectManagerImpl::FinishPreresolveJob(PreresolveJobId job_id, |
| bool success) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PreresolveJob* job = preresolve_jobs_.Lookup(job_id); |
| DCHECK(job); |
| |
| bool need_preconnect = success && job->need_preconnect(); |
| if (need_preconnect) { |
| PreconnectUrl(job->url, job->num_sockets, job->allow_credentials, |
| job->network_anonymization_key, job->traffic_annotation_tag, |
| base::OptionalToPtr(job->storage_partition_config), |
| std::move(job->keepalive_config), |
| std::move(job->connection_change_observer_client)); |
| } |
| |
| PreresolveInfo* info = job->info; |
| if (info) { |
| info->stats->requests_stats.emplace_back(url::Origin::Create(job->url), |
| need_preconnect); |
| } |
| preresolve_jobs_.Remove(job_id); |
| --inflight_preresolves_count_; |
| if (info) { |
| DCHECK_LE(1u, info->inflight_count); |
| --info->inflight_count; |
| } |
| if (info && info->is_done()) { |
| AllPreresolvesForUrlFinished(info); |
| } |
| TryToLaunchPreresolveJobs(); |
| } |
| |
| void PreconnectManagerImpl::AllPreresolvesForUrlFinished(PreresolveInfo* info) { |
| DCHECK(info); |
| DCHECK(info->is_done()); |
| auto it = preresolve_info_.find(info->url); |
| CHECK(it != preresolve_info_.end()); |
| DCHECK(info == it->second.get()); |
| if (delegate_) { |
| delegate_->PreconnectFinished(std::move(info->stats)); |
| } |
| preresolve_info_.erase(it); |
| } |
| |
| network::mojom::NetworkContext* PreconnectManagerImpl::GetNetworkContext( |
| const content::StoragePartitionConfig* storage_partition_config) const { |
| if (network_context_) { |
| return network_context_; |
| } |
| |
| auto* network_context = |
| browser_context_ |
| ->GetStoragePartition( |
| storage_partition_config |
| ? *storage_partition_config |
| : content::StoragePartitionConfig::CreateDefault( |
| browser_context_)) |
| ->GetNetworkContext(); |
| DCHECK(network_context); |
| return network_context; |
| } |
| |
| } // namespace content |