blob: f6d27a8b7734dab2e2f492511752a27205547852 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/predictors/prefetch_manager.h"
#include <utility>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/predictors/perform_network_context_prefetch.h"
#include "chrome/browser/predictors/predictors_features.h"
#include "chrome/browser/predictors/predictors_switches.h"
#include "chrome/browser/predictors/prefetch_traffic_annotation.h"
#include "chrome/browser/predictors/resource_prefetch_predictor.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_request_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/url_loader_throttles.h"
#include "extensions/buildflags/buildflags.h"
#include "net/base/load_flags.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/empty_url_loader_client.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/url_loader_factory_builder.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/loader/throttling_url_loader.h"
#include "third_party/blink/public/common/navigation/preloading_headers.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "extensions/browser/api/web_request/web_request_api.h"
#include "extensions/browser/browser_context_keyed_api_factory.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
namespace predictors {
// This is only defined here because the traffic annotation auditor gets
// confused if you move an annotation to a different file.
const net::NetworkTrafficAnnotationTag kPrefetchTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("predictive_prefetch",
R"(
semantics {
sender: "Loading Predictor"
description:
"This request is issued near the start of a navigation to "
"speculatively fetch resources that resulting page is predicted to "
"request."
trigger:
"Navigating Chrome (by clicking on a link, bookmark, history item, "
"using session restore, etc)."
data:
"Arbitrary site-controlled data can be included in the URL."
"Requests may include cookies and site-specific credentials."
destination: WEBSITE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"There are a number of ways to prevent this request:"
"A) Disable predictive operations under Settings > Advanced > "
" Privacy > Preload pages for faster browsing and searching,"
"B) Disable Lite Mode under Settings > Advanced > Lite mode, or "
"C) Disable 'Make searches and browsing better' under Settings > "
" Sync and Google services > Make searches and browsing better"
chrome_policy {
URLBlocklist {
URLBlocklist: { entries: '*' }
}
}
chrome_policy {
URLAllowlist {
URLAllowlist { }
}
}
}
comments:
"This feature can be safely disabled, but enabling it may result in "
"faster page loads. Using either URLBlocklist or URLAllowlist policies "
"(or a combination of both) limits the scope of these requests."
)");
// Stores the status of all prefetches associated with a given |url|.
struct PrefetchInfo {
PrefetchInfo(const GURL& url, PrefetchManager& manager)
: url(url),
stats(std::make_unique<PrefetchStats>(url)),
manager(&manager) {
DCHECK(url.is_valid());
DCHECK(url.SchemeIsHTTPOrHTTPS());
}
~PrefetchInfo() = default;
PrefetchInfo(const PrefetchInfo&) = delete;
PrefetchInfo& operator=(const PrefetchInfo&) = delete;
void OnJobCreated() { job_count++; }
void OnJobDestroyed() {
job_count--;
if (is_done()) {
// Destroys |this|.
manager->AllPrefetchJobsForUrlFinished(*this);
}
}
bool is_done() const { return job_count == 0; }
GURL url;
size_t job_count = 0;
bool was_canceled = false;
std::unique_ptr<PrefetchStats> stats;
// Owns |this|.
const raw_ptr<PrefetchManager> manager;
base::WeakPtrFactory<PrefetchInfo> weak_factory{this};
};
// Stores all data need for running a prefetch to a |url|.
struct PrefetchJob {
PrefetchJob(PrefetchRequest prefetch_request, PrefetchInfo& info)
: url(prefetch_request.url),
destination(prefetch_request.destination),
creation_time(base::TimeTicks::Now()),
info(info.weak_factory.GetWeakPtr()) {
DCHECK(url.is_valid());
DCHECK(url.SchemeIsHTTPOrHTTPS());
info.OnJobCreated();
}
~PrefetchJob() {
if (info)
info->OnJobDestroyed();
}
PrefetchJob(const PrefetchJob&) = delete;
PrefetchJob& operator=(const PrefetchJob&) = delete;
GURL url;
network::mojom::RequestDestination destination;
base::TimeTicks creation_time;
// PrefetchJob lives until the URL load completes, so it can outlive the
// PrefetchManager and therefore the PrefetchInfo.
base::WeakPtr<PrefetchInfo> info;
};
PrefetchStats::PrefetchStats(const GURL& url)
: url(url), start_time(base::TimeTicks::Now()) {}
PrefetchStats::~PrefetchStats() = default;
PrefetchManager::PrefetchManager(base::WeakPtr<Delegate> delegate,
Profile* profile)
: delegate_(std::move(delegate)),
profile_(profile),
use_network_context_prefetch_(base::FeatureList::IsEnabled(
features::kPrefetchManagerUseNetworkContextPrefetch)) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
}
PrefetchManager::~PrefetchManager() = default;
void PrefetchManager::Start(const GURL& url,
std::vector<PrefetchRequest> requests) {
CHECK(
base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch) ||
base::FeatureList::IsEnabled(blink::features::kLCPPPrefetchSubresource));
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (use_network_context_prefetch_) {
PerformNetworkContextPrefetch(profile_, url, std::move(requests));
return;
}
PrefetchInfo* info;
if (prefetch_info_.find(url) == prefetch_info_.end()) {
auto iterator_and_whether_inserted =
prefetch_info_.emplace(url, std::make_unique<PrefetchInfo>(url, *this));
info = iterator_and_whether_inserted.first->second.get();
} else {
info = prefetch_info_.find(url)->second.get();
}
for (auto& request : requests) {
queued_jobs_.emplace_back(
std::make_unique<PrefetchJob>(std::move(request), *info));
}
TryToLaunchPrefetchJobs();
}
void PrefetchManager::Stop(const GURL& url) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (use_network_context_prefetch_) {
// The NetworkContext Prefetch() functionality does its own cleanup so
// doesn't need to be told to stop.
return;
}
auto it = prefetch_info_.find(url);
if (it == prefetch_info_.end())
return;
it->second->was_canceled = true;
}
// static
bool PrefetchManager::IsAvailableForPrefetch(
network::mojom::RequestDestination destination) {
// TODO(crbug.com/342445996): Expand this for NetworkContextPrefetch once it
// supports more resource types.
return GetResourceTypeForPrefetch(destination).has_value();
}
void PrefetchManager::PrefetchUrl(
std::unique_ptr<PrefetchJob> job,
scoped_refptr<network::SharedURLLoaderFactory> factory) {
CHECK(!use_network_context_prefetch_);
DCHECK(job);
DCHECK(job->info);
PrefetchInfo& info = *job->info;
url::Origin top_frame_origin = url::Origin::Create(info.url);
network::ResourceRequest request;
request.method = "GET";
request.url = job->url;
request.site_for_cookies = net::SiteForCookies::FromUrl(info.url);
request.request_initiator = top_frame_origin;
// The prefetch can happen before the referrer policy is known, so use a
// conservative one (no-referrer) by default.
request.referrer_policy = net::ReferrerPolicy::NO_REFERRER;
// The prefetch can happen before the permissions policy is known, so use a
// conservative, all-blocking permissions policy.
request.permissions_policy =
*network::PermissionsPolicy::CreateFromParsedPolicy(
{}, {}, url::Origin::Create(request.url));
if (!base::FeatureList::IsEnabled(
blink::features::kRemovePurposeHeaderForPrefetch)) {
request.headers.SetHeader(blink::kPurposeHeaderName,
blink::kSecPurposePrefetchHeaderValue);
}
request.headers.SetHeader(blink::kSecPurposeHeaderName,
blink::kSecPurposePrefetchHeaderValue);
request.load_flags = net::LOAD_PREFETCH;
request.destination = job->destination;
auto resource_type = GetResourceTypeForPrefetch(request.destination);
DUMP_WILL_BE_CHECK(resource_type.has_value());
if (!resource_type.has_value()) {
resource_type = blink::mojom::ResourceType::kSubResource;
}
request.resource_type = static_cast<int>(*resource_type);
// TODO(falken): Support CORS?
request.mode = network::mojom::RequestMode::kNoCors;
// The hints are only for requests made from the top frame,
// so frame_origin is the same as top_frame_origin.
auto frame_origin = top_frame_origin;
request.trusted_params = network::ResourceRequest::TrustedParams();
request.trusted_params->isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther, top_frame_origin, frame_origin,
net::SiteForCookies::FromUrl(info.url));
#if BUILDFLAG(ENABLE_EXTENSIONS)
network::URLLoaderFactoryBuilder factory_builder;
auto* web_request_api =
extensions::BrowserContextKeyedAPIFactory<extensions::WebRequestAPI>::Get(
profile_);
if (web_request_api) {
web_request_api->MaybeProxyURLLoaderFactory(
profile_, /*frame=*/nullptr, /*render_process_id=*/0,
content::ContentBrowserClient::URLLoaderFactoryType::kPrefetch,
/*navigation_id=*/std::nullopt, ukm::kInvalidSourceIdObj,
factory_builder, /*header_client=*/nullptr,
/*navigation_response_task_runner=*/nullptr,
/*request_initiator=*/url::Origin());
}
factory = std::move(factory_builder).Finish(factory);
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
// Set up throttles. Use null values for frame/navigation-related params, for
// now, since this is just the browser prefetching resources and the requests
// don't need to appear to come from a frame.
// TODO(falken): Clarify the API of CreateURLLoaderThrottles() for prefetching
// and subresources.
auto wc_getter =
base::BindRepeating([]() -> content::WebContents* { return nullptr; });
std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles =
content::CreateContentBrowserURLLoaderThrottles(
request, profile_, std::move(wc_getter),
/*navigation_ui_data=*/nullptr, content::FrameTreeNodeId(),
/*navigation_id=*/std::nullopt);
auto client = std::make_unique<network::EmptyURLLoaderClient>();
++inflight_jobs_count_;
// Since the CORS-RFC1918 check is skipped when the client security state is
// unknown, just block any local request to be safe for now.
int options = base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kLoadingPredictorAllowLocalRequestForTesting)
? network::mojom::kURLLoadOptionNone
: network::mojom::kURLLoadOptionBlockLocalRequest;
if (base::FeatureList::IsEnabled(
features::kLoadingPredictorPrefetchUseReadAndDiscardBody)) {
options |= network::mojom::kURLLoadOptionReadAndDiscardBody;
}
base::UmaHistogramBoolean("Navigation.Prefetch.IsHttps",
request.url.SchemeIsCryptographic());
std::unique_ptr<blink::ThrottlingURLLoader> loader =
blink::ThrottlingURLLoader::CreateLoaderAndStart(
std::move(factory), std::move(throttles),
content::GlobalRequestID::MakeBrowserInitiated().request_id, options,
&request, client.get(), kPrefetchTrafficAnnotation,
base::SingleThreadTaskRunner::GetCurrentDefault(),
/*cors_exempt_header_list=*/std::nullopt);
delegate_->PrefetchInitiated(info.url, job->url);
// The idea of prefetching is for the network service to put the response in
// the http cache. So from the prefetching layer, nothing needs to be done
// with the response, so just drain it.
auto* raw_client = client.get();
raw_client->Drain(base::BindOnce(&PrefetchManager::OnPrefetchFinished,
weak_factory_.GetWeakPtr(), std::move(job),
std::move(loader), std::move(client)));
}
// Some params are unused but bound to this function to keep them alive until
// the load finishes.
void PrefetchManager::OnPrefetchFinished(
std::unique_ptr<PrefetchJob> job,
std::unique_ptr<blink::ThrottlingURLLoader> loader,
std::unique_ptr<network::mojom::URLLoaderClient> client,
const network::URLLoaderCompletionStatus& status) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
PrefetchInfo& info = *job->info;
if (observer_for_testing_)
observer_for_testing_->OnPrefetchFinished(info.url, job->url, status);
// TODO(ricea): Remove these histograms in October 2024 and make a note of the
// results in https://crbug.com/335524391.
if (status.error_code == net::OK && status.decoded_body_length > 0) {
if (status.decoded_body_length > status.encoded_body_length) {
// Assume it was compressed.
base::UmaHistogramCounts10000(
"Navigation.Prefetch.CompressedBodySize",
static_cast<int>(status.encoded_body_length / 1024));
} else {
// The cast to int will overflow if we prefetch a resource over a terabyte
// in size, but I'm hoping that will never happen.
base::UmaHistogramCounts10000(
"Navigation.Prefetch.UncompressedBodySize",
static_cast<int>(status.encoded_body_length / 1024));
}
}
// Cannot access the fields of `status` after this point.
loader.reset();
client.reset();
job.reset();
--inflight_jobs_count_;
TryToLaunchPrefetchJobs();
}
void PrefetchManager::TryToLaunchPrefetchJobs() {
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.Prefetch.PrefetchJobQueueLength",
queued_jobs_.size());
if (queued_jobs_.empty() || inflight_jobs_count_ >= kMaxInflightPrefetches) {
return;
}
// TODO(falken): Is it ok to assume the default partition? Try to plumb the
// partition here, e.g., from WebContentsObserver. And make a similar change
// in PreconnectManager.
content::StoragePartition* storage_partition =
profile_->GetDefaultStoragePartition();
scoped_refptr<network::SharedURLLoaderFactory> factory =
storage_partition->GetURLLoaderFactoryForBrowserProcess();
while (!queued_jobs_.empty() &&
inflight_jobs_count_ < kMaxInflightPrefetches) {
std::unique_ptr<PrefetchJob> job = std::move(queued_jobs_.front());
queued_jobs_.pop_front();
base::WeakPtr<PrefetchInfo> info = job->info;
// |this| owns all infos.
DCHECK(info);
// Note: PrefetchJobs 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.Prefetch.PrefetchJobQueueingTime",
base::TimeTicks::Now() - job->creation_time);
if (job->url.is_valid() && factory && !info->was_canceled)
PrefetchUrl(std::move(job), factory);
}
}
void PrefetchManager::AllPrefetchJobsForUrlFinished(PrefetchInfo& info) {
DCHECK(info.is_done());
auto it = prefetch_info_.find(info.url);
CHECK(it != prefetch_info_.end());
DCHECK(&info == it->second.get());
if (delegate_)
delegate_->PrefetchFinished(std::move(info.stats));
if (observer_for_testing_)
observer_for_testing_->OnAllPrefetchesFinished(info.url);
prefetch_info_.erase(it);
}
std::optional<blink::mojom::ResourceType> GetResourceTypeForPrefetch(
network::mojom::RequestDestination destination) {
switch (destination) {
case network::mojom::RequestDestination::kEmpty:
return blink::mojom::ResourceType::kSubResource;
case network::mojom::RequestDestination::kScript:
return blink::mojom::ResourceType::kScript;
case network::mojom::RequestDestination::kStyle:
return blink::mojom::ResourceType::kStylesheet;
case network::mojom::RequestDestination::kFont:
return blink::mojom::ResourceType::kFontResource;
case network::mojom::RequestDestination::kAudio:
case network::mojom::RequestDestination::kAudioWorklet:
case network::mojom::RequestDestination::kDocument:
case network::mojom::RequestDestination::kEmbed:
case network::mojom::RequestDestination::kFrame:
case network::mojom::RequestDestination::kIframe:
case network::mojom::RequestDestination::kImage:
case network::mojom::RequestDestination::kManifest:
case network::mojom::RequestDestination::kObject:
case network::mojom::RequestDestination::kPaintWorklet:
case network::mojom::RequestDestination::kReport:
case network::mojom::RequestDestination::kServiceWorker:
case network::mojom::RequestDestination::kSharedWorker:
case network::mojom::RequestDestination::kTrack:
case network::mojom::RequestDestination::kVideo:
case network::mojom::RequestDestination::kWebBundle:
case network::mojom::RequestDestination::kWorker:
case network::mojom::RequestDestination::kXslt:
case network::mojom::RequestDestination::kFencedframe:
case network::mojom::RequestDestination::kWebIdentity:
case network::mojom::RequestDestination::kDictionary:
case network::mojom::RequestDestination::kSpeculationRules:
case network::mojom::RequestDestination::kJson:
case network::mojom::RequestDestination::kSharedStorageWorklet:
return std::nullopt;
}
}
} // namespace predictors