blob: 953987f382f075e13dd17eebcc71d8354de0dc90 [file] [log] [blame]
// Copyright 2018 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/navigation_predictor/navigation_predictor.h"
#include <algorithm>
#include <memory>
#include "base/check_op.h"
#include "base/hash/hash.h"
#include "base/metrics/field_trial_params.h"
#include "base/rand_util.h"
#include "base/ranges/algorithm.h"
#include "base/system/sys_info.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h"
#include "chrome/browser/preloading/prefetch/no_state_prefetch/no_state_prefetch_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/url_canon.h"
namespace {
// The maximum number of clicks to track in a single navigation.
size_t kMaxClicksTracked = 10;
bool IsPrerendering(content::RenderFrameHost& render_frame_host) {
return render_frame_host.GetLifecycleState() ==
content::RenderFrameHost::LifecycleState::kPrerendering;
}
} // namespace
NavigationPredictor::NavigationPredictor(
content::RenderFrameHost& render_frame_host,
mojo::PendingReceiver<AnchorElementMetricsHost> receiver)
: content::DocumentService<blink::mojom::AnchorElementMetricsHost>(
render_frame_host,
std::move(receiver)) {
DETACH_FROM_SEQUENCE(sequence_checker_);
// When using content::Page::IsPrimary, bfcache can cause returning a false in
// the back/forward navigation. So, DCHECK only checks if current page is
// prerendering until deciding how to handle bfcache navigations. See also
// https://crbug.com/1239310.
DCHECK(!IsPrerendering(render_frame_host));
ukm_recorder_ = ukm::UkmRecorder::Get();
ukm_source_id_ = render_frame_host.GetMainFrame()->GetPageUkmSourceId();
}
NavigationPredictor::~NavigationPredictor() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void NavigationPredictor::Create(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::AnchorElementMetricsHost> receiver) {
CHECK(render_frame_host);
DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
DCHECK(!IsPrerendering(*render_frame_host));
// Only valid for the main frame.
if (render_frame_host->GetParentOrOuterDocument())
return;
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
if (!web_contents)
return;
DCHECK(web_contents->GetBrowserContext());
if (web_contents->GetBrowserContext()->IsOffTheRecord()) {
return;
}
// The object is bound to the lifetime of the |render_frame_host| and the mojo
// connection. See DocumentService for details.
new NavigationPredictor(*render_frame_host, std::move(receiver));
}
int NavigationPredictor::GetBucketMinForPageMetrics(int value) const {
return ukm::GetExponentialBucketMin(value, 1.3);
}
int NavigationPredictor::GetLinearBucketForLinkLocation(int value) const {
return ukm::GetLinearBucketMin(static_cast<int64_t>(value), 10);
}
int NavigationPredictor::GetLinearBucketForRatioArea(int value) const {
return ukm::GetLinearBucketMin(static_cast<int64_t>(value), 5);
}
NavigationPredictorMetricsDocumentData&
NavigationPredictor::GetNavigationPredictorMetricsDocumentData() const {
// Create the `NavigationPredictorMetricsDocumentData` object for this
// document if it doesn't already exist.
NavigationPredictorMetricsDocumentData* data =
NavigationPredictorMetricsDocumentData::GetOrCreateForCurrentDocument(
&render_frame_host());
DCHECK(data);
return *data;
}
void NavigationPredictor::ReportNewAnchorElements(
std::vector<blink::mojom::AnchorElementMetricsPtr> elements) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
DCHECK(!IsPrerendering(render_frame_host()));
// Create the AnchorsData object for this WebContents if it doesn't already
// exist. Note that NavigationPredictor only runs on the main frame, but get
// reports for links from all same-process iframes.
NavigationPredictorMetricsDocumentData::AnchorsData& data =
GetNavigationPredictorMetricsDocumentData().GetAnchorsData();
GURL document_url;
std::vector<GURL> new_predictions;
for (auto& element : elements) {
AnchorId anchor_id(element->anchor_id);
if (anchors_.find(anchor_id) != anchors_.end()) {
continue;
}
data.number_of_anchors_++;
if (element->contains_image) {
data.number_of_anchors_contains_image_++;
}
if (element->is_url_incremented_by_one) {
data.number_of_anchors_url_incremented_++;
}
if (element->is_in_iframe) {
data.number_of_anchors_in_iframe_++;
}
if (element->is_same_host) {
data.number_of_anchors_same_host_++;
}
data.viewport_height_ = element->viewport_size.height();
data.viewport_width_ = element->viewport_size.width();
data.total_clickable_space_ += element->ratio_area * 100;
data.link_locations_.push_back(element->ratio_distance_top_to_visible_top);
// Collect the target URL if it is new, without ref (# fragment).
GURL::Replacements replacements;
replacements.ClearRef();
document_url = element->source_url.ReplaceComponents(replacements);
GURL target_url = element->target_url.ReplaceComponents(replacements);
if (target_url != document_url &&
predicted_urls_.find(target_url) == predicted_urls_.end()) {
predicted_urls_.insert(target_url);
new_predictions.push_back(target_url);
}
anchors_.emplace(anchor_id, std::move(element));
tracked_anchor_id_to_index_[anchor_id] = tracked_anchor_id_to_index_.size();
}
if (!new_predictions.empty()) {
NavigationPredictorKeyedService* service =
NavigationPredictorKeyedServiceFactory::GetForProfile(
Profile::FromBrowserContext(
render_frame_host().GetBrowserContext()));
DCHECK(service);
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host());
service->OnPredictionUpdated(
web_contents, document_url,
NavigationPredictorKeyedService::PredictionSource::
kAnchorElementsParsedFromWebPage,
new_predictions);
}
}
void NavigationPredictor::ReportAnchorElementClick(
blink::mojom::AnchorElementClickPtr click) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
DCHECK(!IsPrerendering(render_frame_host()));
navigation_start_to_click_ = click->navigation_start_to_click;
clicked_count_++;
if (clicked_count_ > kMaxClicksTracked)
return;
if (!ukm_recorder_) {
return;
}
// An anchor index of -1 indicates that we are not going to log details about
// the anchor that was clicked.
int anchor_index = -1;
AnchorId anchor_id(click->anchor_id);
auto index_it = tracked_anchor_id_to_index_.find(anchor_id);
if (index_it != tracked_anchor_id_to_index_.end()) {
anchor_index = index_it->second;
// Record PreloadOnHover.HoverTakenMs and PreloadOnHover.PointerDownTakenMs
// to UKM.
auto& navigation_predictor_metrics_data =
GetNavigationPredictorMetricsDocumentData();
auto& user_interactions =
navigation_predictor_metrics_data.GetUserInteractionsData();
auto& user_interaction = user_interactions[index_it->second];
// navigation_start_to_click_ is set to click->navigation_start_to_click and
// should always has a value.
CHECK(navigation_start_to_click_.has_value());
if (user_interaction.last_navigation_start_to_pointer_over.has_value() ||
user_interaction.last_navigation_start_to_last_pointer_down
.has_value()) {
NavigationPredictorMetricsDocumentData::PreloadOnHoverData
preload_on_hover;
preload_on_hover.taken = true;
if (user_interaction.last_navigation_start_to_pointer_over.has_value()) {
// `hover_dwell_time` measures the time delta from the last mouse over
// event to the last mouse click event.
preload_on_hover.hover_dwell_time =
navigation_start_to_click_.value() -
user_interaction.last_navigation_start_to_pointer_over.value();
}
if (user_interaction.last_navigation_start_to_last_pointer_down
.has_value()) {
// `pointer_down_duration` measures the time delta from the last mouse
// down event to the last mouse click event.
preload_on_hover.pointer_down_duration =
navigation_start_to_click_.value() -
user_interaction.last_navigation_start_to_last_pointer_down.value();
user_interaction.last_navigation_start_to_last_pointer_down.reset();
}
navigation_predictor_metrics_data.AddPreloadOnHoverData(
std::move(preload_on_hover));
}
}
NavigationPredictorMetricsDocumentData::PageLinkClickData page_link_click;
page_link_click.anchor_element_index_ = anchor_index;
auto it = anchors_.find(anchor_id);
if (it != anchors_.end()) {
page_link_click.href_unchanged_ =
(it->second->target_url == click->target_url);
}
navigation_start_to_click_ = click->navigation_start_to_click;
// navigation_start_to_click_ is set to click->navigation_start_to_click and
// should always has a value.
CHECK(navigation_start_to_click_.has_value());
GetNavigationPredictorMetricsDocumentData().SetNavigationStartToClick(
navigation_start_to_click_.value());
page_link_click.navigation_start_to_link_clicked_ =
navigation_start_to_click_.value();
GetNavigationPredictorMetricsDocumentData().AddPageLinkClickData(
std::move(page_link_click));
}
void NavigationPredictor::ReportAnchorElementsLeftViewport(
std::vector<blink::mojom::AnchorElementLeftViewportPtr> elements) {
auto& user_interactions =
GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
for (const auto& element : elements) {
auto index_it =
tracked_anchor_id_to_index_.find(AnchorId(element->anchor_id));
if (index_it == tracked_anchor_id_to_index_.end()) {
continue;
}
auto& user_interaction = user_interactions[index_it->second];
user_interaction.is_in_viewport = false;
user_interaction.last_navigation_start_to_entered_viewport.reset();
user_interaction.max_time_in_viewport = std::max(
user_interaction.max_time_in_viewport.value_or(base::TimeDelta()),
element->time_in_viewport);
}
}
void NavigationPredictor::ReportAnchorElementPointerOver(
blink::mojom::AnchorElementPointerOverPtr pointer_over_event) {
auto& user_interactions =
GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
auto index_it =
tracked_anchor_id_to_index_.find(AnchorId(pointer_over_event->anchor_id));
if (index_it == tracked_anchor_id_to_index_.end()) {
return;
}
auto& user_interaction = user_interactions[index_it->second];
if (!user_interaction.is_hovered) {
user_interaction.pointer_hovering_over_count++;
}
user_interaction.is_hovered = true;
user_interaction.last_navigation_start_to_pointer_over =
pointer_over_event->navigation_start_to_pointer_over;
}
void NavigationPredictor::ReportAnchorElementPointerOut(
blink::mojom::AnchorElementPointerOutPtr hover_event) {
auto& navigation_predictor_metrics_data =
GetNavigationPredictorMetricsDocumentData();
auto& user_interactions =
navigation_predictor_metrics_data.GetUserInteractionsData();
auto index_it =
tracked_anchor_id_to_index_.find(AnchorId(hover_event->anchor_id));
if (index_it == tracked_anchor_id_to_index_.end()) {
return;
}
auto& user_interaction = user_interactions[index_it->second];
// Record PreloadOnHover.HoverNotTakenMs and
// PreloadOnHover.MouseDownNotTakenMs to UKM.
NavigationPredictorMetricsDocumentData::PreloadOnHoverData preload_on_hover;
preload_on_hover.taken = false;
preload_on_hover.hover_dwell_time = hover_event->hover_dwell_time;
if (user_interaction.last_navigation_start_to_last_pointer_down.has_value() &&
user_interaction.last_navigation_start_to_pointer_over.has_value()) {
preload_on_hover.pointer_down_duration =
user_interaction.last_navigation_start_to_pointer_over.value() +
hover_event->hover_dwell_time -
user_interaction.last_navigation_start_to_last_pointer_down.value();
user_interaction.last_navigation_start_to_last_pointer_down.reset();
}
navigation_predictor_metrics_data.AddPreloadOnHoverData(
std::move(preload_on_hover));
// Update user interactions.
user_interaction.is_hovered = false;
user_interaction.last_navigation_start_to_pointer_over.reset();
user_interaction.max_hover_dwell_time = std::max(
hover_event->hover_dwell_time,
user_interaction.max_hover_dwell_time.value_or(base::TimeDelta()));
}
void NavigationPredictor::ReportAnchorElementPointerDown(
blink::mojom::AnchorElementPointerDownPtr pointer_down_event) {
auto index_it =
tracked_anchor_id_to_index_.find(AnchorId(pointer_down_event->anchor_id));
if (index_it == tracked_anchor_id_to_index_.end()) {
return;
}
auto& user_interactions =
GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
auto& user_interaction = user_interactions[index_it->second];
user_interaction.last_navigation_start_to_last_pointer_down =
pointer_down_event->navigation_start_to_pointer_down;
}
void NavigationPredictor::ReportAnchorElementsEnteredViewport(
std::vector<blink::mojom::AnchorElementEnteredViewportPtr> elements) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
DCHECK(!IsPrerendering(render_frame_host()));
if (elements.empty()) {
return;
}
auto& navigation_predictor_metrics_data =
GetNavigationPredictorMetricsDocumentData();
auto& user_interactions =
navigation_predictor_metrics_data.GetUserInteractionsData();
for (const auto& element : elements) {
AnchorId anchor_id(element->anchor_id);
auto index_it = tracked_anchor_id_to_index_.find(anchor_id);
if (index_it == tracked_anchor_id_to_index_.end()) {
// We're not tracking this element, no need to generate a
// NavigationPredictorAnchorElementMetrics record.
continue;
}
auto& user_interaction = user_interactions[index_it->second];
user_interaction.is_in_viewport = true;
user_interaction.last_navigation_start_to_entered_viewport =
element->navigation_start_to_entered_viewport;
auto anchor_it = anchors_.find(anchor_id);
if (anchor_it == anchors_.end()) {
// We don't know about this anchor, likely because at its first paint,
// AnchorElementMetricsSender didn't send it to NavigationPredictor.
// Reasons could be that the link had non-HTTP scheme, the anchor had
// zero width/height, etc.
continue;
}
const auto& anchor = anchor_it->second;
// Collect the target URL if it is new, without ref (# fragment).
GURL::Replacements replacements;
replacements.ClearRef();
GURL document_url = anchor->source_url.ReplaceComponents(replacements);
GURL target_url = anchor->target_url.ReplaceComponents(replacements);
if (target_url == document_url) {
// Ignore anchors pointing to the same document.
continue;
}
if (!ukm_recorder_) {
continue;
}
NavigationPredictorMetricsDocumentData::AnchorElementMetricsData metrics;
metrics.is_in_iframe_ = anchor->is_in_iframe;
metrics.is_url_incremented_by_one_ = anchor->is_url_incremented_by_one;
metrics.contains_image_ = anchor->contains_image;
metrics.is_same_origin_ = anchor->is_same_host;
metrics.has_text_sibling_ = anchor->has_text_sibling;
metrics.is_bold_ = anchor->font_weight > 500;
metrics.navigation_start_to_link_logged =
element->navigation_start_to_entered_viewport;
if (anchor->font_size_px < 10) {
metrics.font_size_ = 1;
} else if (anchor->font_size_px < 18) {
metrics.font_size_ = 2;
} else {
metrics.font_size_ = 3;
}
base::StringPiece path = anchor->target_url.path_piece();
int64_t path_length = path.length();
path_length = ukm::GetLinearBucketMin(path_length, 10);
// Truncate at 100 characters.
metrics.path_length_ = std::min(path_length, static_cast<int64_t>(100));
int64_t num_slashes = base::ranges::count(path, '/');
// Truncate at 5.
metrics.path_depth_ = std::min(num_slashes, static_cast<int64_t>(5));
// 10-bucket hash of the URL's path.
uint32_t hash = base::PersistentHash(path.data(), path.length());
metrics.bucketed_path_hash_ = hash % 10;
// Convert the ratio area and ratio distance from [0,1] to [0,100].
int percent_ratio_area = static_cast<int>(anchor->ratio_area * 100);
metrics.percent_clickable_area_ =
GetLinearBucketForRatioArea(percent_ratio_area);
int percent_ratio_distance_root_top =
static_cast<int>(anchor->ratio_distance_root_top * 100);
metrics.percent_vertical_distance_ =
GetLinearBucketForLinkLocation(percent_ratio_distance_root_top);
navigation_predictor_metrics_data.AddAnchorElementMetricsData(
index_it->second, std::move(metrics));
}
}