Commit f2854a0d authored by Tarun Bansal's avatar Tarun Bansal Committed by Commit Bot

Navigation Predictor: Add logic to preconnect to origins with highest preconnect score

At onLoad, Preconnect to the origin that has the highest preconnect
score as long as the score is above the specified threshold.
Currently, due to privacy constraints, this is restricted to
only the document origin.

Effectively, this means that we preconnect to the document origin
only if its preconnect score is more than the specified threshold.
If not, no preconnect action is taken.

The preconnect score of an origin is computed from the score
of all the anchor elements on the webpage that point to that
origin.

Change-Id: Ie3178f352cb5106124e13cb6f7e39b9ecf81174c
Bug: 903945
Reviewed-on: https://chromium-review.googlesource.com/c/1349874Reviewed-by: default avatarRyan Sturm <ryansturm@chromium.org>
Commit-Queue: Tarun Bansal <tbansal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611955}
parent f78e0392
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
#include "mojo/public/cpp/bindings/strong_binding.h" #include "mojo/public/cpp/bindings/strong_binding.h"
#include "third_party/blink/public/common/features.h" #include "third_party/blink/public/common/features.h"
#include "url/gurl.h" #include "url/gurl.h"
#include "url/origin.h"
namespace { namespace {
...@@ -112,9 +111,14 @@ NavigationPredictor::NavigationPredictor( ...@@ -112,9 +111,14 @@ NavigationPredictor::NavigationPredictor(
prefetch_url_score_threshold_(base::GetFieldTrialParamByFeatureAsInt( prefetch_url_score_threshold_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible, blink::features::kRecordAnchorMetricsVisible,
"prefetch_url_score_threshold", "prefetch_url_score_threshold",
0)),
preconnect_origin_score_threshold_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"preconnect_origin_score_threshold",
0)) { 0)) {
DCHECK(browser_context_); DCHECK(browser_context_);
DETACH_FROM_SEQUENCE(sequence_checker_); DETACH_FROM_SEQUENCE(sequence_checker_);
DCHECK_LE(0, preconnect_origin_score_threshold_);
DCHECK_LE(0, prefetch_url_score_threshold_); DCHECK_LE(0, prefetch_url_score_threshold_);
if (render_frame_host != GetMainFrame(render_frame_host)) if (render_frame_host != GetMainFrame(render_frame_host))
...@@ -182,7 +186,7 @@ void NavigationPredictor::RecordActionAccuracyOnClick( ...@@ -182,7 +186,7 @@ void NavigationPredictor::RecordActionAccuracyOnClick(
static constexpr char histogram_name_non_dse[] = static constexpr char histogram_name_non_dse[] =
"NavigationPredictor.OnNonDSE.AccuracyActionTaken"; "NavigationPredictor.OnNonDSE.AccuracyActionTaken";
if (!prefetch_url_) { if (!prefetch_url_ && !preconnect_origin_) {
base::UmaHistogramEnumeration(source_is_default_search_engine_page_ base::UmaHistogramEnumeration(source_is_default_search_engine_page_
? histogram_name_dse ? histogram_name_dse
: histogram_name_non_dse, : histogram_name_non_dse,
...@@ -190,6 +194,26 @@ void NavigationPredictor::RecordActionAccuracyOnClick( ...@@ -190,6 +194,26 @@ void NavigationPredictor::RecordActionAccuracyOnClick(
return; return;
} }
// Exactly one action must have been taken.
DCHECK(prefetch_url_.has_value() != preconnect_origin_.has_value());
if (preconnect_origin_) {
if (url::Origin::Create(target_url) == preconnect_origin_) {
base::UmaHistogramEnumeration(
source_is_default_search_engine_page_ ? histogram_name_dse
: histogram_name_non_dse,
ActionAccuracy::kPreconnectActionClickToSameOrigin);
return;
}
base::UmaHistogramEnumeration(
source_is_default_search_engine_page_ ? histogram_name_dse
: histogram_name_non_dse,
ActionAccuracy::kPreconnectActionClickToDifferentOrigin);
return;
}
DCHECK(prefetch_url_);
if (target_url == prefetch_url_.value()) { if (target_url == prefetch_url_.value()) {
base::UmaHistogramEnumeration( base::UmaHistogramEnumeration(
source_is_default_search_engine_page_ ? histogram_name_dse source_is_default_search_engine_page_ ? histogram_name_dse
...@@ -680,8 +704,10 @@ void NavigationPredictor::MaybeTakeActionOnLoad( ...@@ -680,8 +704,10 @@ void NavigationPredictor::MaybeTakeActionOnLoad(
? "NavigationPredictor.OnDSE.ActionTaken" ? "NavigationPredictor.OnDSE.ActionTaken"
: "NavigationPredictor.OnNonDSE.ActionTaken"; : "NavigationPredictor.OnNonDSE.ActionTaken";
DCHECK(!preconnect_origin_.has_value());
DCHECK(!prefetch_url_.has_value()); DCHECK(!prefetch_url_.has_value());
// Try prefetch first.
prefetch_url_ = GetUrlToPrefetch(document_origin, sorted_navigation_scores); prefetch_url_ = GetUrlToPrefetch(document_origin, sorted_navigation_scores);
if (prefetch_url_.has_value()) { if (prefetch_url_.has_value()) {
DCHECK_EQ(document_origin.host(), prefetch_url_->host()); DCHECK_EQ(document_origin.host(), prefetch_url_->host());
...@@ -689,6 +715,15 @@ void NavigationPredictor::MaybeTakeActionOnLoad( ...@@ -689,6 +715,15 @@ void NavigationPredictor::MaybeTakeActionOnLoad(
return; return;
} }
// Compute preconnect origin only if there is no valid prefetch URL.
preconnect_origin_ =
GetOriginToPreconnect(document_origin, sorted_navigation_scores);
if (preconnect_origin_.has_value()) {
DCHECK_EQ(document_origin.host(), preconnect_origin_->host());
base::UmaHistogramEnumeration(action_histogram_name, Action::kPreconnect);
return;
}
base::UmaHistogramEnumeration(action_histogram_name, Action::kNone); base::UmaHistogramEnumeration(action_histogram_name, Action::kNone);
} }
...@@ -725,6 +760,84 @@ base::Optional<GURL> NavigationPredictor::GetUrlToPrefetch( ...@@ -725,6 +760,84 @@ base::Optional<GURL> NavigationPredictor::GetUrlToPrefetch(
return sorted_navigation_scores[0]->url; return sorted_navigation_scores[0]->url;
} }
base::Optional<url::Origin> NavigationPredictor::GetOriginToPreconnect(
const url::Origin& document_origin,
const std::vector<std::unique_ptr<NavigationScore>>&
sorted_navigation_scores) const {
// On search engine results page, next navigation is likely to be a different
// origin. Currently, the preconnect is only allowed for same origins. Hence,
// preconnect is currently disabled on search engine results page.
if (source_is_default_search_engine_page_)
return base::nullopt;
// Compute preconnect score for each origins: Multiple anchor elements on
// the webpage may point to the same origin. The preconnect score for an
// origin is computed by taking sum of score of all anchor elements that
// point to that origin.
std::map<url::Origin, double> preconnect_score_by_origin_map;
for (const auto& navigation_score : sorted_navigation_scores) {
const url::Origin origin = url::Origin::Create(navigation_score->url);
auto iter = preconnect_score_by_origin_map.find(origin);
if (iter == preconnect_score_by_origin_map.end()) {
preconnect_score_by_origin_map[origin] = navigation_score->score;
} else {
double& existing_metric = iter->second;
existing_metric += navigation_score->score;
}
}
struct ScoreByOrigin {
url::Origin origin;
double score;
ScoreByOrigin(const url::Origin& origin, double score)
: origin(origin), score(score) {}
};
// |sorted_preconnect_scores| would contain preconnect scores of different
// origins sorted in descending order of the preconnect score.
std::vector<ScoreByOrigin> sorted_preconnect_scores;
// First copy all entries from |preconnect_score_by_origin_map| to
// |sorted_preconnect_scores|.
for (const auto& score_by_origin_map_entry : preconnect_score_by_origin_map) {
ScoreByOrigin entry(score_by_origin_map_entry.first,
score_by_origin_map_entry.second);
sorted_preconnect_scores.push_back(entry);
}
if (sorted_preconnect_scores.empty())
return base::nullopt;
// Sort scores by the calculated preconnect score in descending order.
std::sort(sorted_preconnect_scores.begin(), sorted_preconnect_scores.end(),
[](const auto& a, const auto& b) { return a.score > b.score; });
#if DCHECK_IS_ON()
// |sum_of_scores| must be close to the total score of 100.
double sum_of_scores = 0.0;
for (const auto& score_by_origin : sorted_preconnect_scores)
sum_of_scores += score_by_origin.score;
// Allow an error of 2.0. i.e., |sum_of_scores| is expected to be between 98
// and 102.
DCHECK_GE(2.0, std::abs(sum_of_scores - 100));
#endif
// Connect to the origin with highest score provided the origin is same
// as the document origin.
if (sorted_preconnect_scores[0].origin != document_origin)
return base::nullopt;
// If the prediction score of the highest scoring origin is less than the
// threshold, then return.
if (sorted_preconnect_scores[0].score < preconnect_origin_score_threshold_) {
return base::nullopt;
}
return sorted_preconnect_scores[0].origin;
}
void NavigationPredictor::RecordMetricsOnLoad( void NavigationPredictor::RecordMetricsOnLoad(
const blink::mojom::AnchorElementMetrics& metric) const { const blink::mojom::AnchorElementMetrics& metric) const {
DCHECK(!browser_context_->IsOffTheRecord()); DCHECK(!browser_context_->IsOffTheRecord());
......
...@@ -17,16 +17,13 @@ ...@@ -17,16 +17,13 @@
#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_observer.h"
#include "mojo/public/cpp/bindings/interface_request.h" #include "mojo/public/cpp/bindings/interface_request.h"
#include "third_party/blink/public/mojom/loader/navigation_predictor.mojom.h" #include "third_party/blink/public/mojom/loader/navigation_predictor.mojom.h"
#include "url/origin.h"
namespace content { namespace content {
class BrowserContext; class BrowserContext;
class RenderFrameHost; class RenderFrameHost;
} }
namespace url {
class Origin;
}
class SiteEngagementService; class SiteEngagementService;
class TemplateURLService; class TemplateURLService;
...@@ -78,10 +75,21 @@ class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost, ...@@ -78,10 +75,21 @@ class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost,
// Navigation predictor prefetched a URL, and an anchor element was clicked // Navigation predictor prefetched a URL, and an anchor element was clicked
// whose URL had a different origin than the prefetched URL. // whose URL had a different origin than the prefetched URL.
kPrefetchActionClickToDifferentOrigin = 3, kPrefetchActionClickToDifferentOrigin = 3,
kMaxValue = kPrefetchActionClickToDifferentOrigin,
// Navigation predictor preconnected to an origin, and an anchor element was
// clicked whose URL had the same origin as the preconnected origin.
kPreconnectActionClickToSameOrigin = 4,
// Navigation predictor preconnected to an origin, and an anchor element was
// clicked whose URL had a different origin than the preconnected origin.
kPreconnectActionClickToDifferentOrigin = 5,
kMaxValue = kPreconnectActionClickToDifferentOrigin,
}; };
protected: protected:
// Origin that we decided to preconnect to.
base::Optional<url::Origin> preconnect_origin_;
// URL that we decided to prefetch. // URL that we decided to prefetch.
base::Optional<GURL> prefetch_url_; base::Optional<GURL> prefetch_url_;
...@@ -139,6 +147,11 @@ class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost, ...@@ -139,6 +147,11 @@ class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost,
const std::vector<std::unique_ptr<NavigationScore>>& const std::vector<std::unique_ptr<NavigationScore>>&
sorted_navigation_scores) const; sorted_navigation_scores) const;
base::Optional<url::Origin> GetOriginToPreconnect(
const url::Origin& document_origin,
const std::vector<std::unique_ptr<NavigationScore>>&
sorted_navigation_scores) const;
// Record anchor element metrics on page load. // Record anchor element metrics on page load.
void RecordMetricsOnLoad( void RecordMetricsOnLoad(
const blink::mojom::AnchorElementMetrics& metric) const; const blink::mojom::AnchorElementMetrics& metric) const;
...@@ -184,9 +197,16 @@ class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost, ...@@ -184,9 +197,16 @@ class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost,
// True if device is a low end device. // True if device is a low end device.
const bool is_low_end_device_; const bool is_low_end_device_;
// Minimum score that a URL should have for it to be prefetched. // Minimum score that a URL should have for it to be prefetched. Note
// that scores of origins are computed differently from scores of URLs, so
// they are not comparable.
const int prefetch_url_score_threshold_; const int prefetch_url_score_threshold_;
// Minimum preconnect score that the origin should have for preconnect. Note
// that scores of origins are computed differently from scores of URLs, so
// they are not comparable.
const int preconnect_origin_score_threshold_;
// Timing of document loaded and last click. // Timing of document loaded and last click.
base::TimeTicks document_loaded_timing_; base::TimeTicks document_loaded_timing_;
base::TimeTicks last_click_timing_; base::TimeTicks last_click_timing_;
......
...@@ -38,6 +38,10 @@ class TestNavigationPredictor : public NavigationPredictor { ...@@ -38,6 +38,10 @@ class TestNavigationPredictor : public NavigationPredictor {
base::Optional<GURL> prefetch_url() const { return prefetch_url_; } base::Optional<GURL> prefetch_url() const { return prefetch_url_; }
base::Optional<url::Origin> preconnect_origin() const {
return preconnect_origin_;
}
const std::map<GURL, int>& GetAreaRankMap() const { return area_rank_map_; } const std::map<GURL, int>& GetAreaRankMap() const { return area_rank_map_; }
private: private:
...@@ -66,7 +70,8 @@ class NavigationPredictorTest : public ChromeRenderViewHostTestHarness { ...@@ -66,7 +70,8 @@ class NavigationPredictorTest : public ChromeRenderViewHostTestHarness {
~NavigationPredictorTest() override = default; ~NavigationPredictorTest() override = default;
void SetUp() override { void SetUp() override {
SetupFieldTrial(base::nullopt /* prefetch_url_score_threshold */); SetupFieldTrial(base::nullopt /* preconnect_origin_score_threshold */,
base::nullopt /* prefetch_url_score_threshold */);
ChromeRenderViewHostTestHarness::SetUp(); ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>( predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
mojo::MakeRequest(&predictor_service_), main_rfh()); mojo::MakeRequest(&predictor_service_), main_rfh());
...@@ -96,12 +101,21 @@ class NavigationPredictorTest : public ChromeRenderViewHostTestHarness { ...@@ -96,12 +101,21 @@ class NavigationPredictorTest : public ChromeRenderViewHostTestHarness {
return predictor_service_helper_->prefetch_url(); return predictor_service_helper_->prefetch_url();
} }
base::Optional<url::Origin> preconnect_origin() const {
return predictor_service_helper_->preconnect_origin();
}
protected: protected:
void SetupFieldTrial(base::Optional<int> prefetch_url_score_threshold) { void SetupFieldTrial(base::Optional<int> preconnect_origin_score_threshold,
base::Optional<int> prefetch_url_score_threshold) {
const std::string kTrialName = "TrialFoo2"; const std::string kTrialName = "TrialFoo2";
const std::string kGroupName = "GroupFoo2"; // Value not used const std::string kGroupName = "GroupFoo2"; // Value not used
std::map<std::string, std::string> params; std::map<std::string, std::string> params;
if (preconnect_origin_score_threshold.has_value()) {
params["preconnect_origin_score_threshold"] =
base::IntToString(preconnect_origin_score_threshold.value());
}
if (prefetch_url_score_threshold.has_value()) { if (prefetch_url_score_threshold.has_value()) {
params["prefetch_url_score_threshold"] = params["prefetch_url_score_threshold"] =
base::IntToString(prefetch_url_score_threshold.value()); base::IntToString(prefetch_url_score_threshold.value());
...@@ -374,7 +388,8 @@ TEST_F(NavigationPredictorTest, ...@@ -374,7 +388,8 @@ TEST_F(NavigationPredictorTest,
class NavigationPredictorPrefetchDisabledTest : public NavigationPredictorTest { class NavigationPredictorPrefetchDisabledTest : public NavigationPredictorTest {
public: public:
void SetUp() override { void SetUp() override {
SetupFieldTrial(101 /* prefetch_url_score_threshold */); SetupFieldTrial(0 /* preconnect_origin_score_threshold */,
101 /* prefetch_url_score_threshold */);
ChromeRenderViewHostTestHarness::SetUp(); ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>( predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
...@@ -382,7 +397,103 @@ class NavigationPredictorPrefetchDisabledTest : public NavigationPredictorTest { ...@@ -382,7 +397,103 @@ class NavigationPredictorPrefetchDisabledTest : public NavigationPredictorTest {
} }
}; };
// Disables prefetch and loads a page where the preconnect score of the document
// origin is highest among all origins. Verifies that navigation predictor
// preconnects to the document origin.
TEST_F(NavigationPredictorPrefetchDisabledTest,
ActionTaken_SameOrigin_Prefetch_BelowThreshold) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
// Cross origin anchor element is small. This should result in example.com to
// have the highest preconnect score.
const std::string diff_origin_href_xsmall = "https://example2.com/xsmall";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xsmall, 0.0001));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics));
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kPreconnect, 1);
EXPECT_FALSE(prefetch_url().has_value());
EXPECT_EQ(url::Origin::Create(GURL(source)), preconnect_origin());
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectTotalCount(
"AnchorElementMetrics.Clicked.HrefEngagementScore2", 1);
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.AccuracyActionTaken",
NavigationPredictor::ActionAccuracy::kPreconnectActionClickToSameOrigin,
1);
}
// Disables prefetch and loads a page where the preconnect score of a cross
// origin is highest among all origins. Verifies that navigation predictor does
// not preconnect to the cross origin.
TEST_F(NavigationPredictorPrefetchDisabledTest, TEST_F(NavigationPredictorPrefetchDisabledTest,
ActionTaken_PreconnectHighScoreIsCrossOrigin) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
// Cross origin anchor element is large. This should result in example2.com to
// have the highest preconnect score.
const std::string diff_origin_href_xlarge = "https://example2.com/xlarge";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xlarge, 10));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics));
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kNone, 1);
EXPECT_FALSE(prefetch_url().has_value());
EXPECT_FALSE(preconnect_origin().has_value());
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.AccuracyActionTaken",
NavigationPredictor::ActionAccuracy::kNoActionTakenClickHappened, 1);
}
// Framework for testing cases where preconnect and prefetch are effectively
// disabled by setting their thresholds as too high.
class NavigationPredictorPreconnectPrefetchDisabledTest
: public NavigationPredictorTest {
public:
void SetUp() override {
SetupFieldTrial(101 /* preconnect_origin_score_threshold */,
101 /* prefetch_url_score_threshold */);
ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
mojo::MakeRequest(&predictor_service_), main_rfh());
}
};
// No action should be taken when both preconnect and prefetch are effectively
// disabled.
TEST_F(NavigationPredictorPreconnectPrefetchDisabledTest,
ActionTaken_SameOrigin_Prefetch_BelowThreshold) { ActionTaken_SameOrigin_Prefetch_BelowThreshold) {
const std::string source = "https://example.com"; const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small"; const std::string same_origin_href_small = "https://example.com/small";
......
...@@ -34615,6 +34615,12 @@ Called by update_use_counter_css.py.--> ...@@ -34615,6 +34615,12 @@ Called by update_use_counter_css.py.-->
prefetch URL"/> prefetch URL"/>
<int value="3" <int value="3"
label="Click was to a URL with different origin than the prefetch URL"/> label="Click was to a URL with different origin than the prefetch URL"/>
<int value="4"
label="Click was to a URL with the same origin as the preconnected
origin"/>
<int value="5"
label="Click was to a URL with different origin than the preconnected
origin"/>
</enum> </enum>
<enum name="NavigationPredictorActionTaken"> <enum name="NavigationPredictorActionTaken">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment