blob: 9d3e41e66530050e7948a0ddc23a6c5122381c2f [file] [log] [blame]
// Copyright 2023 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/preloading_decider.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/elapsed_timer.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/preloading/preloading.h"
#include "content/browser/preloading/preloading_data_impl.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/preloading_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/preloading/anchor_element_interaction_host.mojom.h"
namespace content {
class PreloadingDeciderBrowserTest : public ContentBrowserTest {
public:
PreloadingDeciderBrowserTest() = default;
~PreloadingDeciderBrowserTest() override = default;
void SetUp() override {
feature_list_.InitWithFeaturesAndParameters(
{
{blink::features::kPreloadingHeuristicsMLModel,
{{"enact_candidates", "true"}}},
{blink::features::kPrerender2InNewTab, {}},
},
{
// Disable the memory requirement of Prerender2 so the test can run
// on any bot.
{blink::features::kPrerender2MemoryControls},
});
ContentBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_->AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(https_server_->Start());
}
WebContents* web_contents() { return shell()->web_contents(); }
const GURL GetTestURL(const char* file) const {
return https_server_->GetURL(file);
}
void ExpectCandidatesReceived() {
// The renderer queues updates and waits for style to be clean.
EXPECT_EQ(true, EvalJsAfterLifecycleUpdate(web_contents(), "", "true"));
EXPECT_EQ(true, EvalJsAfterLifecycleUpdate(web_contents(), "", "true"));
auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(
web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider);
EXPECT_TRUE(preloading_decider->HasCandidatesForTesting());
}
private:
content::test::PreloadingConfigOverride preloading_config_override_;
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<net::EmbeddedTestServer> https_server_;
};
IN_PROC_BROWSER_TEST_F(PreloadingDeciderBrowserTest,
SetIsNavigationInDomainCallback) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(),
GetTestURL("/preloading/preloading_decider.html")));
ExpectCandidatesReceived();
// Now navigate to another page
TestNavigationObserver nav_observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "document.getElementById('bar').click()"));
nav_observer.Wait();
histogram_tester.ExpectBucketCount(
"Preloading.Predictor.SpeculationRules.Recall",
PredictorConfusionMatrix::kFalseNegative, 1);
histogram_tester.ExpectBucketCount(
"Preloading.Predictor.UrlPointerDownOnAnchor.Recall",
PredictorConfusionMatrix::kFalseNegative, 1);
histogram_tester.ExpectBucketCount(
"Preloading.Predictor.UrlPointerHoverOnAnchor.Recall",
PredictorConfusionMatrix::kFalseNegative, 1);
histogram_tester.ExpectBucketCount(
"Preloading.Predictor.PreloadingHeuristicsMLModel.Recall",
PredictorConfusionMatrix::kFalseNegative, 1);
}
class PreloadingDeciderNonEagerBrowserTest
: public PreloadingDeciderBrowserTest,
public ::testing::WithParamInterface<
::testing::tuple<PreloadingPredictor, PreloadingType>> {
public:
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
const auto& [predictor, type] = info.param;
return base::StringPrintf(
"%s_%s", std::string(predictor.name()).c_str(),
std::string(PreloadingTypeToString(type)).c_str());
}
static constexpr PreloadingPredictor kNonEagerPredictors[] = {
preloading_predictor::kUrlPointerDownOnAnchor,
preloading_predictor::kUrlPointerHoverOnAnchor,
preloading_predictor::kPreloadingHeuristicsMLModel,
};
static constexpr PreloadingType kPreloadingTypes[] = {
PreloadingType::kPrefetch,
PreloadingType::kPrerender,
};
PreloadingPredictor predictor() const {
return ::testing::get<0>(GetParam());
}
PreloadingType type() const { return ::testing::get<1>(GetParam()); }
};
INSTANTIATE_TEST_SUITE_P(
All,
PreloadingDeciderNonEagerBrowserTest,
::testing::Combine(
::testing::ValuesIn(
PreloadingDeciderNonEagerBrowserTest::kNonEagerPredictors),
::testing::ValuesIn(
PreloadingDeciderNonEagerBrowserTest::kPreloadingTypes)),
PreloadingDeciderNonEagerBrowserTest::DescribeParams);
IN_PROC_BROWSER_TEST_P(PreloadingDeciderNonEagerBrowserTest,
EnactModerateCandidate) {
base::ScopedMockElapsedTimersForTest mock_elapsed_timer;
base::HistogramTester histogram_tester;
ukm::TestAutoSetUkmRecorder ukm_recorder;
ASSERT_TRUE(NavigateToURL(
shell(), GetTestURL("/preloading/preloading_decider_moderate.html")));
ExpectCandidatesReceived();
std::string next_page_id;
GURL next_page_url;
switch (type()) {
case PreloadingType::kPrefetch:
next_page_id = "b";
next_page_url = GetTestURL("/title1.html?b");
break;
case PreloadingType::kPrerender:
next_page_id = "c";
next_page_url = GetTestURL("/title1.html?c");
break;
default:
FAIL();
}
// Trigger the non-eager predictor.
auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(
web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider);
if (predictor() == preloading_predictor::kUrlPointerDownOnAnchor) {
preloading_decider->OnPointerDown(next_page_url);
} else if (predictor() == preloading_predictor::kUrlPointerHoverOnAnchor) {
preloading_decider->OnPointerHover(
next_page_url,
blink::mojom::AnchorElementPointerData::New(true, 0.0, 0.0));
} else if (predictor() ==
preloading_predictor::kPreloadingHeuristicsMLModel) {
preloading_decider->OnPreloadingHeuristicsModelDone(next_page_url,
/*score=*/1.0);
} else {
FAIL();
}
TestNavigationObserver nav_observer(web_contents());
EXPECT_TRUE(
ExecJs(web_contents(),
JsReplace("document.getElementById($1).click()", next_page_id)));
nav_observer.Wait();
const ukm::SourceId source_id = nav_observer.next_page_ukm_source_id();
const std::string type_str{PreloadingTypeToString(type())};
const char* type_cstr = type_str.c_str();
// For non-eager predictors, there are two PreloadingPredictors that
// contribute to a preloading attempt. One creates a candidate, but does not
// start the preloading attempt. The other starts the attempt. We assert below
// that both predictors have appropriate attribution in the recorded metrics.
{
const std::string rule_predictor_str{
content_preloading_predictor::kSpeculationRules.name()};
const char* rule_predictor_cstr = rule_predictor_str.c_str();
// We intentionally don't record a prediction for non-eager speculation
// rules. They aren't predictions per se, but a declaration to the browser
// that preloading would be safe.
histogram_tester.ExpectTotalCount(
base::StringPrintf("Preloading.Predictor.%s.Precision",
rule_predictor_cstr),
0);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Recall",
rule_predictor_cstr),
PredictorConfusionMatrix::kFalseNegative, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.%s.Attempt.%s.Precision", type_cstr,
rule_predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.%s.Attempt.%s.Recall", type_cstr,
rule_predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.%s.Attempt.%s.TriggeringOutcome",
type_cstr, rule_predictor_cstr),
PreloadingTriggeringOutcome::kSuccess, 1);
test::PreloadingAttemptUkmEntryBuilder attempt_entry_builder(
content_preloading_predictor::kSpeculationRules);
ukm::TestUkmRecorder::HumanReadableUkmEntry expected_attempt_entry =
attempt_entry_builder.BuildEntry(
source_id, type(), PreloadingEligibility::kEligible,
PreloadingHoldbackStatus::kAllowed,
PreloadingTriggeringOutcome::kSuccess,
PreloadingFailureReason::kUnspecified, /*accurate=*/true,
base::ScopedMockElapsedTimersForTest::kMockElapsedTime,
blink::mojom::SpeculationEagerness::kModerate);
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> attempts =
ukm_recorder.GetEntries(ukm::builders::Preloading_Attempt::kEntryName,
test::kPreloadingAttemptUkmMetrics);
std::erase_if(attempts, [&](const auto& entry) {
return entry.metrics.at(
ukm::builders::Preloading_Attempt::kPreloadingPredictorName) !=
content_preloading_predictor::kSpeculationRules.ukm_value();
});
if (base::FeatureList::IsEnabled(
features::kPrerender2FallbackPrefetchSpecRules) &&
type() == PreloadingType::kPrerender) {
// If type is prerender, `PrerendererImpl` triggers prefetch ahead
// prerender. Ignore the corresponding UKM as it is checkid in
// prerenderer_impl_browsertest.cc.
ASSERT_EQ(attempts.size(), 2u);
EXPECT_EQ(attempts[1], expected_attempt_entry)
<< test::ActualVsExpectedUkmEntryToString(attempts[1],
expected_attempt_entry);
} else {
ASSERT_EQ(attempts.size(), 1u);
EXPECT_EQ(attempts[0], expected_attempt_entry)
<< test::ActualVsExpectedUkmEntryToString(attempts[0],
expected_attempt_entry);
}
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> predictions =
ukm_recorder.GetEntries(
ukm::builders::Preloading_Prediction::kEntryName,
test::kPreloadingPredictionUkmMetrics);
std::erase_if(predictions, [&](const auto& entry) {
return entry.metrics.at(ukm::builders::Preloading_Prediction::
kPreloadingPredictorName) !=
content_preloading_predictor::kSpeculationRules.ukm_value();
});
EXPECT_TRUE(predictions.empty());
}
{
const std::string predictor_str{predictor().name()};
const char* predictor_cstr = predictor_str.c_str();
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Precision", predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Recall", predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.%s.Attempt.%s.Precision", type_cstr,
predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.%s.Attempt.%s.Recall", type_cstr,
predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.%s.Attempt.%s.TriggeringOutcome",
type_cstr, predictor_cstr),
PreloadingTriggeringOutcome::kSuccess, 1);
constexpr bool accurate = true;
constexpr int confidence = 100;
test::PreloadingAttemptUkmEntryBuilder attempt_entry_builder(predictor());
ukm::TestUkmRecorder::HumanReadableUkmEntry expected_attempt_entry =
attempt_entry_builder.BuildEntry(
source_id, type(), PreloadingEligibility::kEligible,
PreloadingHoldbackStatus::kAllowed,
PreloadingTriggeringOutcome::kSuccess,
PreloadingFailureReason::kUnspecified, accurate,
base::ScopedMockElapsedTimersForTest::kMockElapsedTime,
blink::mojom::SpeculationEagerness::kModerate);
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> attempts =
ukm_recorder.GetEntries(ukm::builders::Preloading_Attempt::kEntryName,
test::kPreloadingAttemptUkmMetrics);
std::erase_if(attempts, [&](const auto& entry) {
return entry.metrics.at(
ukm::builders::Preloading_Attempt::kPreloadingPredictorName) !=
predictor().ukm_value();
});
if (base::FeatureList::IsEnabled(
features::kPrerender2FallbackPrefetchSpecRules) &&
type() == PreloadingType::kPrerender) {
// If type is prerender, `PrerendererImpl` triggers prefetch ahead
// prerender. Ignore the corresponding UKM as it is checkid in
// prerenderer_impl_browsertest.cc.
ASSERT_EQ(attempts.size(), 2u);
EXPECT_EQ(attempts[1], expected_attempt_entry)
<< test::ActualVsExpectedUkmEntryToString(attempts[1],
expected_attempt_entry);
} else {
ASSERT_EQ(attempts.size(), 1u);
EXPECT_EQ(attempts[0], expected_attempt_entry)
<< test::ActualVsExpectedUkmEntryToString(attempts[0],
expected_attempt_entry);
}
test::PreloadingPredictionUkmEntryBuilder prediction_entry_builder(
predictor());
ukm::TestUkmRecorder::HumanReadableUkmEntry expected_prediction_entry =
prediction_entry_builder.BuildEntry(source_id, confidence, accurate);
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> predictions =
ukm_recorder.GetEntries(
ukm::builders::Preloading_Prediction::kEntryName,
test::kPreloadingPredictionUkmMetrics);
std::erase_if(predictions, [&](const auto& entry) {
return entry.metrics.at(ukm::builders::Preloading_Prediction::
kPreloadingPredictorName) !=
predictor().ukm_value();
});
ASSERT_EQ(predictions.size(), 1u);
EXPECT_EQ(predictions[0], expected_prediction_entry)
<< test::ActualVsExpectedUkmEntryToString(predictions[0],
expected_prediction_entry);
}
}
IN_PROC_BROWSER_TEST_F(PreloadingDeciderBrowserTest,
EnactModerateNewTabPrerender) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(
shell(), GetTestURL("/preloading/preloading_decider_moderate.html")));
ExpectCandidatesReceived();
const GURL new_tab_url = GetTestURL("/title1.html?d");
auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(
web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider);
TestNavigationObserver nav_observer(new_tab_url);
nav_observer.StartWatchingNewWebContents();
preloading_decider->OnPointerDown(new_tab_url);
EXPECT_TRUE(ExecJs(web_contents(), "document.getElementById('d').click()"));
nav_observer.Wait();
// Also navigate the current tab away, so any of its metrics are flushed.
ASSERT_TRUE(NavigateToURL(shell(), GetTestURL("/title2.html")));
{
const std::string rule_predictor_str{
content_preloading_predictor::kSpeculationRules.name()};
const char* rule_predictor_cstr = rule_predictor_str.c_str();
// We intentionally don't record a prediction for non-eager speculation
// rules. They aren't predictions per se, but a declaration to the browser
// that preloading would be safe.
histogram_tester.ExpectTotalCount(
base::StringPrintf("Preloading.Predictor.%s.Precision",
rule_predictor_cstr),
0);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Recall",
rule_predictor_cstr),
PredictorConfusionMatrix::kFalseNegative, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Prerender.Attempt.%s.Precision",
rule_predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Prerender.Attempt.%s.Recall",
rule_predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
}
{
const std::string predictor_str{
preloading_predictor::kUrlPointerDownOnAnchor.name()};
const char* predictor_cstr = predictor_str.c_str();
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Precision", predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Recall", predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Prerender.Attempt.%s.Precision",
predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Prerender.Attempt.%s.Recall",
predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
}
}
IN_PROC_BROWSER_TEST_F(PreloadingDeciderBrowserTest, PredictionWithoutAttempt) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(
shell(), GetTestURL("/preloading/preloading_decider_moderate.html")));
ExpectCandidatesReceived();
auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(
web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider);
TestNavigationObserver nav_observer(web_contents());
preloading_decider->OnPointerDown(GetTestURL("/title1.html?a"));
EXPECT_TRUE(ExecJs(web_contents(), "document.getElementById('a').click()"));
nav_observer.Wait();
{
const std::string predictor_str{
preloading_predictor::kUrlPointerDownOnAnchor.name()};
const char* predictor_cstr = predictor_str.c_str();
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Precision", predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Predictor.%s.Recall", predictor_cstr),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectTotalCount(
base::StringPrintf("Preloading.Prerender.Attempt.%s.Precision",
predictor_cstr),
0);
histogram_tester.ExpectUniqueSample(
base::StringPrintf("Preloading.Prerender.Attempt.%s.Recall",
predictor_cstr),
PredictorConfusionMatrix::kFalseNegative, 1);
}
}
} // namespace content