blob: 495677fbcf2fee59d59e9b3453eb5774c8b16dd4 [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_data_impl.h"
#include <algorithm>
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/preloading/preloading.h"
#include "content/browser/preloading/preloading_confidence.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_web_contents.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
namespace content {
class PreloadingDataImplTest : public RenderViewHostTestHarness {
public:
PreloadingDataImplTest() = default;
PreloadingDataImplTest(const PreloadingDataImplTest&) = delete;
PreloadingDataImplTest& operator=(const PreloadingDataImplTest&) = delete;
void SetUp() override {
RenderViewHostTestHarness::SetUp();
browser_context_ = std::make_unique<TestBrowserContext>();
web_contents_ = TestWebContents::Create(
browser_context_.get(),
SiteInstanceImpl::Create(browser_context_.get()));
}
void TearDown() override {
web_contents_.reset();
browser_context_.reset();
RenderViewHostTestHarness::TearDown();
}
WebContents* GetWebContents() { return web_contents_.get(); }
std::string UmaPredictorPrecision(const PreloadingPredictor& predictor) {
return base::StrCat(
{"Preloading.Predictor.", predictor.name(), ".Precision"});
}
std::string UmaPredictorRecall(const PreloadingPredictor& predictor) {
return base::StrCat({"Preloading.Predictor.", predictor.name(), ".Recall"});
}
std::string UmaAttemptPrecision(const PreloadingPredictor& predictor,
PreloadingType preloading_type) {
return base::StrCat({"Preloading.", PreloadingTypeToString(preloading_type),
".Attempt.", predictor.name(), ".Precision"});
}
std::string UmaAttemptRecall(const PreloadingPredictor& predictor,
PreloadingType preloading_type) {
return base::StrCat({"Preloading.", PreloadingTypeToString(preloading_type),
".Attempt.", predictor.name(), ".Recall"});
}
private:
std::unique_ptr<TestBrowserContext> browser_context_;
std::unique_ptr<TestWebContents> web_contents_;
};
TEST_F(PreloadingDataImplTest, PredictorPrecisionAndRecall) {
base::HistogramTester histogram_tester;
auto* preloading_data =
PreloadingDataImpl::GetOrCreateForWebContents(GetWebContents());
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerDownOnAnchor,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerHoverOnAnchor,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kLinkRel,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
// Add preloading predictions.
GURL url_1{"https://www.example.com/page1.html"};
GURL url_2{"https://www.example.com/page2.html"};
GURL url_3{"https://www.example.com/page3.html"};
const auto target = url_1;
PreloadingPredictor predictor_1{
preloading_predictor::kUrlPointerDownOnAnchor};
PreloadingPredictor predictor_2{
preloading_predictor::kUrlPointerHoverOnAnchor};
PreloadingPredictor predictor_3{preloading_predictor::kLinkRel};
ukm::SourceId triggered_primary_page_source_id =
GetWebContents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
preloading_data->AddPreloadingPrediction(
predictor_1, PreloadingConfidence{100},
PreloadingData::GetSameURLMatcher(url_1),
triggered_primary_page_source_id);
preloading_data->AddPreloadingPrediction(
predictor_1, PreloadingConfidence{100},
PreloadingData::GetSameURLMatcher(url_1),
triggered_primary_page_source_id);
preloading_data->AddPreloadingPrediction(
predictor_1, PreloadingConfidence{100},
PreloadingData::GetSameURLMatcher(url_2),
triggered_primary_page_source_id);
preloading_data->AddPreloadingPrediction(
predictor_2, PreloadingConfidence{100},
PreloadingData::GetSameURLMatcher(url_2),
triggered_primary_page_source_id);
preloading_data->AddPreloadingPrediction(
predictor_2, PreloadingConfidence{100},
PreloadingData::GetSameURLMatcher(url_3),
triggered_primary_page_source_id);
NavigationSimulator::NavigateAndCommitFromBrowser(GetWebContents(), target);
// Check precision UKM records.
// Since, we added the predictor twice, it should count the true positives
// twice as well.
histogram_tester.ExpectBucketCount(UmaPredictorPrecision(predictor_1),
PredictorConfusionMatrix::kTruePositive,
2);
histogram_tester.ExpectBucketCount(UmaPredictorPrecision(predictor_1),
PredictorConfusionMatrix::kFalsePositive,
1);
histogram_tester.ExpectBucketCount(UmaPredictorPrecision(predictor_2),
PredictorConfusionMatrix::kTruePositive,
0);
histogram_tester.ExpectBucketCount(UmaPredictorPrecision(predictor_2),
PredictorConfusionMatrix::kFalsePositive,
2);
histogram_tester.ExpectTotalCount(UmaPredictorPrecision(predictor_3), 0);
// Check recall UKM records.
// It should only record 1 TP and not 2, and also no FN.
histogram_tester.ExpectBucketCount(UmaPredictorRecall(predictor_1),
PredictorConfusionMatrix::kTruePositive,
1);
histogram_tester.ExpectBucketCount(UmaPredictorRecall(predictor_1),
PredictorConfusionMatrix::kFalseNegative,
0);
// It should only record 1 FN.
histogram_tester.ExpectBucketCount(UmaPredictorRecall(predictor_2),
PredictorConfusionMatrix::kTruePositive,
0);
histogram_tester.ExpectBucketCount(UmaPredictorRecall(predictor_2),
PredictorConfusionMatrix::kFalseNegative,
1);
// For the missing predictor we should record 1 FN.
histogram_tester.ExpectBucketCount(UmaPredictorRecall(predictor_3),
PredictorConfusionMatrix::kTruePositive,
0);
histogram_tester.ExpectBucketCount(UmaPredictorRecall(predictor_3),
PredictorConfusionMatrix::kFalseNegative,
1);
}
TEST_F(PreloadingDataImplTest, PageLoadWithoutAttemptIsFalseNegative) {
base::HistogramTester histogram_tester;
auto* preloading_data =
PreloadingDataImpl::GetOrCreateForWebContents(GetWebContents());
const PreloadingPredictor predictor{
preloading_predictor::kUrlPointerDownOnAnchor};
preloading_data->SetIsNavigationInDomainCallback(
predictor,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
const GURL target{"https://www.example.com/page1.html"};
NavigationSimulator::NavigateAndCommitFromBrowser(GetWebContents(), target);
// The lack of an attempt represents a false negative.
histogram_tester.ExpectUniqueSample(
UmaAttemptRecall(predictor, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kFalseNegative, 1);
histogram_tester.ExpectUniqueSample(
UmaAttemptRecall(predictor, PreloadingType::kPrerender),
PredictorConfusionMatrix::kFalseNegative, 1);
}
TEST_F(PreloadingDataImplTest, PreloadingAttemptPrecisionAndRecall) {
base::HistogramTester histogram_tester;
auto* preloading_data =
PreloadingDataImpl::GetOrCreateForWebContents(GetWebContents());
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerDownOnAnchor,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerHoverOnAnchor,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kLinkRel,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return false; }));
// Add preloading predictions.
GURL url_1{"https://www.example.com/page1.html"};
GURL url_2{"https://www.example.com/page2.html"};
GURL url_3{"https://www.example.com/page3.html"};
const auto target = url_1;
PreloadingPredictor predictor_1{
preloading_predictor::kUrlPointerDownOnAnchor};
PreloadingPredictor predictor_2{
preloading_predictor::kUrlPointerHoverOnAnchor};
PreloadingPredictor predictor_3{preloading_predictor::kLinkRel};
std::vector<std::tuple<PreloadingPredictor, PreloadingType, GURL>> attempts{
{predictor_1, PreloadingType::kPrerender, url_1},
{predictor_2, PreloadingType::kPrefetch, url_2},
{predictor_2, PreloadingType::kPrerender, url_1},
{predictor_2, PreloadingType::kPrerender, url_2},
{predictor_2, PreloadingType::kPrerender, url_3},
};
for (const auto& [predictor, preloading_type, url] : attempts) {
preloading_data->AddPreloadingAttempt(
predictor, preloading_type, PreloadingData::GetSameURLMatcher(url),
GetWebContents()->GetPrimaryMainFrame()->GetPageUkmSourceId());
}
NavigationSimulator::NavigateAndCommitFromBrowser(GetWebContents(), target);
// Check precision UKM records.
// There should be no UMA records for predictor_1, prefetch attempt.
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_1, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kTruePositive, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_1, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kFalsePositive, 0);
// There should 1 TP and 0 FP for predictor_1, prerender attempt.
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_1, PreloadingType::kPrerender),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_1, PreloadingType::kPrerender),
PredictorConfusionMatrix::kFalsePositive, 0);
// There should 0 TP and 1 FP for predictor_2, prefetch attempt.
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_2, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kTruePositive, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_2, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kFalsePositive, 1);
// There should 1 TP and 2 FP for predictor_2, prerender attempt.
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_2, PreloadingType::kPrerender),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectBucketCount(
UmaAttemptPrecision(predictor_2, PreloadingType::kPrerender),
PredictorConfusionMatrix::kFalsePositive, 2);
// Check recall UKM records.
// predictor_1, prerender should be TP
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_1, PreloadingType::kPrerender),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_1, PreloadingType::kPrerender),
PredictorConfusionMatrix::kFalseNegative, 0);
// predictor_1, prefetch should be FN
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_1, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kTruePositive, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_1, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kFalseNegative, 1);
// predictor_2, prerender should be TP
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_2, PreloadingType::kPrerender),
PredictorConfusionMatrix::kTruePositive, 1);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_2, PreloadingType::kPrerender),
PredictorConfusionMatrix::kFalseNegative, 0);
// predictor_2, prefetch should be FN
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_2, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kTruePositive, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_2, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kFalseNegative, 1);
// 'page_in_domain_check' returns false for predictor_3: TP=0, FN=0.
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_3, PreloadingType::kPrerender),
PredictorConfusionMatrix::kTruePositive, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_3, PreloadingType::kPrerender),
PredictorConfusionMatrix::kFalseNegative, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_3, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kTruePositive, 0);
histogram_tester.ExpectBucketCount(
UmaAttemptRecall(predictor_3, PreloadingType::kPrefetch),
PredictorConfusionMatrix::kFalseNegative, 0);
}
namespace {
void RunSamplingTest(WebContents* web_contents,
int num_predictions,
int expected_sampling_amount_bucket) {
using Preloading_Prediction = ukm::builders::Preloading_Prediction;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
ukm::SourceId triggered_primary_page_source_id =
web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId();
auto* preloading_data =
PreloadingDataImpl::GetOrCreateForWebContents(web_contents);
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerHoverOnAnchor,
base::BindRepeating(
[](NavigationHandle* /*navigation_handle*/) { return true; }));
// Add a number of predictions. If they're beyond the limit, we should only
// keep a random sample to stay within the limit.
preloading_data->SetMaxPredictionsToTenForTesting();
const size_t expected_predictions_size = std::min(10, num_predictions);
for (int i = 0; i < num_predictions; ++i) {
preloading_data->AddPreloadingPrediction(
preloading_predictor::kUrlPointerHoverOnAnchor,
PreloadingConfidence{100},
PreloadingData::GetSameURLMatcher(
GURL(base::StrCat({"https://www.example.com/page",
base::NumberToString(i), ".html"}))),
triggered_primary_page_source_id);
}
EXPECT_EQ(expected_predictions_size,
preloading_data->GetPredictionsSizeForTesting());
NavigationSimulator::NavigateAndCommitFromBrowser(
web_contents, GURL("https://www.example.com/somewhere_else.html"));
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> actual_ukm_entries =
test_ukm_recorder.GetEntries(
Preloading_Prediction::kEntryName,
{Preloading_Prediction::kSamplingAmountName});
EXPECT_EQ(expected_predictions_size, actual_ukm_entries.size());
for (const auto& entry : actual_ukm_entries) {
EXPECT_EQ(expected_sampling_amount_bucket,
entry.metrics.at(Preloading_Prediction::kSamplingAmountName));
}
}
} // namespace
TEST_F(PreloadingDataImplTest, MaxPredictions) {
constexpr double kBucketSpacing = 1.3;
RunSamplingTest(GetWebContents(), /*num_predictions=*/20,
/*expected_sampling_amount_bucket=*/
ukm::GetExponentialBucketMin(500'000, kBucketSpacing));
}
TEST_F(PreloadingDataImplTest, NoSamplingUnderMaxPredictions) {
RunSamplingTest(GetWebContents(), /*num_predictions=*/5,
/*expected_sampling_amount_bucket=*/0);
}
} // namespace content