blob: ea07832d8e599bc13a7dd294690c38fc41975c57 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/predictors/loading_predictor_tab_helper.h"
#include <set>
#include <string>
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/browser/predictors/loading_predictor_factory.h"
#include "chrome/browser/predictors/predictors_enums.h"
#include "chrome/browser/predictors/predictors_features.h"
#include "chrome/browser/profiles/profile.h"
#include "components/optimization_guide/optimization_guide_decider.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
using content::BrowserThread;
namespace predictors {
namespace {
constexpr char kLoadingPredictorOptimizationHintsReceiveStatusHistogram[] =
"LoadingPredictor.OptimizationHintsReceiveStatus";
// Called only for subresources.
net::RequestPriority GetRequestPriority(
network::mojom::RequestDestination request_destination) {
switch (request_destination) {
case network::mojom::RequestDestination::kStyle:
case network::mojom::RequestDestination::kFont:
return net::HIGHEST;
case network::mojom::RequestDestination::kScript:
return net::MEDIUM;
case network::mojom::RequestDestination::kEmpty:
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::kWorker:
case network::mojom::RequestDestination::kXslt:
return net::LOWEST;
}
}
bool IsHandledNavigation(content::NavigationHandle* navigation_handle) {
return navigation_handle->IsInMainFrame() &&
!navigation_handle->IsSameDocument() &&
navigation_handle->GetURL().SchemeIsHTTPOrHTTPS();
}
network::mojom::RequestDestination GetDestination(
optimization_guide::proto::ResourceType type) {
switch (type) {
case optimization_guide::proto::RESOURCE_TYPE_UNKNOWN:
return network::mojom::RequestDestination::kEmpty;
case optimization_guide::proto::RESOURCE_TYPE_CSS:
return network::mojom::RequestDestination::kStyle;
case optimization_guide::proto::RESOURCE_TYPE_SCRIPT:
return network::mojom::RequestDestination::kScript;
}
}
bool ShouldPrefetchDestination(network::mojom::RequestDestination destination) {
switch (features::kLoadingPredictorPrefetchSubresourceType.Get()) {
case features::PrefetchSubresourceType::kAll:
return true;
case features::PrefetchSubresourceType::kCss:
return destination == network::mojom::RequestDestination::kStyle;
case features::PrefetchSubresourceType::kJsAndCss:
return destination == network::mojom::RequestDestination::kScript ||
destination == network::mojom::RequestDestination::kStyle;
}
NOTREACHED();
return false;
}
// Util class for recording the status for when we received optimization hints
// for navigations that we requested them for.
class ScopedOptimizationHintsReceiveStatusRecorder {
public:
ScopedOptimizationHintsReceiveStatusRecorder()
: status_(OptimizationHintsReceiveStatus::kUnknown) {}
~ScopedOptimizationHintsReceiveStatusRecorder() {
DCHECK_NE(status_, OptimizationHintsReceiveStatus::kUnknown);
UMA_HISTOGRAM_ENUMERATION(
kLoadingPredictorOptimizationHintsReceiveStatusHistogram, status_);
}
void set_status(OptimizationHintsReceiveStatus status) { status_ = status; }
private:
OptimizationHintsReceiveStatus status_;
};
} // namespace
LoadingPredictorTabHelper::LoadingPredictorTabHelper(
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
auto* predictor = LoadingPredictorFactory::GetForProfile(profile);
if (predictor)
predictor_ = predictor->GetWeakPtr();
if (base::FeatureList::IsEnabled(
features::kLoadingPredictorUseOptimizationGuide)) {
optimization_guide_decider_ =
OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
if (optimization_guide_decider_) {
optimization_guide_decider_->RegisterOptimizationTypes(
{optimization_guide::proto::LOADING_PREDICTOR});
}
}
}
LoadingPredictorTabHelper::~LoadingPredictorTabHelper() = default;
void LoadingPredictorTabHelper::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!predictor_)
return;
if (!IsHandledNavigation(navigation_handle))
return;
auto navigation_id = NavigationID(
web_contents(),
ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID),
navigation_handle->GetURL(), navigation_handle->NavigationStart());
if (!navigation_id.is_valid())
return;
current_navigation_id_ = navigation_id;
if (predictor_->OnNavigationStarted(navigation_id))
return;
if (!optimization_guide_decider_)
return;
last_optimization_guide_prediction_ = OptimizationGuidePrediction();
last_optimization_guide_prediction_->decision =
optimization_guide::OptimizationGuideDecision::kUnknown;
// We do not have any predictions on device, so consult the optimization
// guide.
optimization_guide_decider_->CanApplyOptimizationAsync(
navigation_handle, optimization_guide::proto::LOADING_PREDICTOR,
base::BindOnce(&LoadingPredictorTabHelper::OnOptimizationGuideDecision,
weak_ptr_factory_.GetWeakPtr(), navigation_id));
}
void LoadingPredictorTabHelper::DidRedirectNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!predictor_)
return;
if (!IsHandledNavigation(navigation_handle))
return;
auto navigation_id = NavigationID(
web_contents(),
ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID),
navigation_handle->GetURL(), navigation_handle->NavigationStart());
if (!navigation_id.is_valid())
return;
current_navigation_id_ = navigation_id;
// If we are not trying to get an optimization guide prediction for this page
// load, just return.
if (!optimization_guide_decider_ || !last_optimization_guide_prediction_)
return;
// Get an updated prediction for the navigation.
optimization_guide_decider_->CanApplyOptimizationAsync(
navigation_handle, optimization_guide::proto::LOADING_PREDICTOR,
base::BindOnce(&LoadingPredictorTabHelper::OnOptimizationGuideDecision,
weak_ptr_factory_.GetWeakPtr(), navigation_id));
}
void LoadingPredictorTabHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!predictor_)
return;
if (!IsHandledNavigation(navigation_handle))
return;
// Clear out the current navigation since there is not one in flight anymore.
current_navigation_id_ = NavigationID();
auto old_navigation_id =
NavigationID(web_contents(),
ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID),
navigation_handle->GetRedirectChain().front(),
navigation_handle->NavigationStart());
auto new_navigation_id = NavigationID(
web_contents(),
ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID),
navigation_handle->GetURL(), navigation_handle->NavigationStart());
if (!old_navigation_id.is_valid() || !new_navigation_id.is_valid())
return;
predictor_->OnNavigationFinished(old_navigation_id, new_navigation_id,
navigation_handle->IsErrorPage());
}
void LoadingPredictorTabHelper::ResourceLoadComplete(
content::RenderFrameHost* render_frame_host,
const content::GlobalRequestID& request_id,
const blink::mojom::ResourceLoadInfo& resource_load_info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!predictor_)
return;
bool is_main_frame = render_frame_host->GetParent() == nullptr;
if (!is_main_frame)
return;
auto navigation_id = NavigationID(web_contents());
if (!navigation_id.is_valid())
return;
predictor_->loading_data_collector()->RecordResourceLoadComplete(
navigation_id, resource_load_info);
}
void LoadingPredictorTabHelper::DidLoadResourceFromMemoryCache(
const GURL& url,
const std::string& mime_type,
network::mojom::RequestDestination request_destination) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!predictor_)
return;
auto navigation_id = NavigationID(web_contents());
if (!navigation_id.is_valid())
return;
blink::mojom::ResourceLoadInfo resource_load_info;
resource_load_info.original_url = url;
resource_load_info.final_url = url;
resource_load_info.mime_type = mime_type;
resource_load_info.request_destination = request_destination;
resource_load_info.method = "GET";
resource_load_info.request_priority =
GetRequestPriority(resource_load_info.request_destination);
resource_load_info.network_info =
blink::mojom::CommonNetworkInfo::New(false, false, base::nullopt);
predictor_->loading_data_collector()->RecordResourceLoadComplete(
navigation_id, resource_load_info);
}
void LoadingPredictorTabHelper::DocumentOnLoadCompletedInMainFrame() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!predictor_)
return;
auto navigation_id = NavigationID(web_contents());
if (!navigation_id.is_valid())
return;
predictor_->loading_data_collector()->RecordMainFrameLoadComplete(
navigation_id, last_optimization_guide_prediction_);
// Clear out Optimization Guide Prediction, as it is no longer needed.
last_optimization_guide_prediction_ = base::nullopt;
}
void LoadingPredictorTabHelper::OnOptimizationGuideDecision(
const NavigationID& navigation_id,
optimization_guide::OptimizationGuideDecision decision,
const optimization_guide::OptimizationMetadata& metadata) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(navigation_id.is_valid());
if (!predictor_)
return;
ScopedOptimizationHintsReceiveStatusRecorder recorder;
if (current_navigation_id_.is_valid() &&
current_navigation_id_ != navigation_id) {
// The current navigation has either redirected or a new one has started, so
// return.
recorder.set_status(
OptimizationHintsReceiveStatus::kAfterRedirectOrNextNavigationStart);
return;
}
if (!current_navigation_id_.is_valid()) {
// There is not a pending navigation.
recorder.set_status(OptimizationHintsReceiveStatus::kAfterNavigationFinish);
auto last_committed_navigation_id = NavigationID(web_contents());
if (!last_committed_navigation_id.is_valid() ||
last_committed_navigation_id != navigation_id) {
// This hint is no longer relevant, so return.
return;
}
// If we get here, we have not navigated away from the navigation we
// received hints for. Proceed to get the preconnect prediction so we can
// see how the predicted resources match what was actually fetched for
// counterfactual logging purposes.
} else {
recorder.set_status(
OptimizationHintsReceiveStatus::kBeforeNavigationFinish);
}
if (!last_optimization_guide_prediction_) {
// Data for the navigation has already been recorded, do not proceed any
// further, even for counterfactual logging.
return;
}
last_optimization_guide_prediction_->decision = decision;
last_optimization_guide_prediction_->optimization_guide_prediction_arrived =
base::TimeTicks::Now();
if (decision != optimization_guide::OptimizationGuideDecision::kTrue)
return;
if (!metadata.loading_predictor_metadata()) {
// Metadata is not applicable, so just log an unknown decision.
last_optimization_guide_prediction_->decision =
optimization_guide::OptimizationGuideDecision::kUnknown;
return;
}
PreconnectPrediction prediction;
url::Origin main_frame_origin =
url::Origin::Create(navigation_id.main_frame_url);
net::NetworkIsolationKey network_isolation_key(main_frame_origin,
main_frame_origin);
std::set<url::Origin> predicted_origins;
std::vector<GURL> predicted_subresources;
const auto lp_metadata = metadata.loading_predictor_metadata();
for (const auto& subresource : lp_metadata->subresources()) {
GURL subresource_url(subresource.url());
if (!subresource_url.is_valid())
continue;
predicted_subresources.push_back(subresource_url);
if (base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch)) {
network::mojom::RequestDestination destination =
GetDestination(subresource.resource_type());
if (ShouldPrefetchDestination(destination)) {
// TODO(falken): Detect duplicates.
prediction.prefetch_requests.emplace_back(
subresource_url, network_isolation_key, destination);
}
} else {
url::Origin subresource_origin = url::Origin::Create(subresource_url);
if (subresource_origin == main_frame_origin) {
// We are already connecting to the main frame origin by default, so
// don't include this in the prediction.
continue;
}
if (predicted_origins.find(subresource_origin) != predicted_origins.end())
continue;
predicted_origins.insert(subresource_origin);
prediction.requests.emplace_back(subresource_origin, 1,
network_isolation_key);
}
}
last_optimization_guide_prediction_->preconnect_prediction = prediction;
last_optimization_guide_prediction_->predicted_subresources =
predicted_subresources;
// Only preconnect if the navigation is still pending and we want to use the
// predictions to preconnect to subresource origins.
if (current_navigation_id_.is_valid() &&
features::ShouldUseOptimizationGuidePredictionsToPreconnect()) {
predictor_->PrepareForPageLoad(navigation_id.main_frame_url,
HintOrigin::OPTIMIZATION_GUIDE,
/*preconnectable=*/false, prediction);
}
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(LoadingPredictorTabHelper)
} // namespace predictors