blob: 4fe74771f81b0b7ee1eb89dafa49c2c1529ee0aa [file] [log] [blame]
// Copyright 2016 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 <algorithm>
#include <cstddef>
#include <set>
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/test/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/browsing_data/browsing_data_helper.h"
#include "chrome/browser/predictors/loading_data_collector.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/browser/predictors/loading_predictor_factory.h"
#include "chrome/browser/predictors/loading_stats_collector.h"
#include "chrome/browser/predictors/loading_test_util.h"
#include "chrome/browser/predictors/resource_prefetch_common.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/test/test_utils.h"
#include "net/base/host_port_pair.h"
#include "net/base/url_util.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace predictors {
namespace {
const char kLocalHost[] = "127.0.0.1";
const char kFooHost[] = "foo.com";
const char kBarHost[] = "bar.com";
const char kBazHost[] = "baz.com";
const char kImageMime[] = "image/png";
const char kStyleMime[] = "text/css";
const char kJavascriptMime[] = "application/javascript";
// Paths to resources handled by a custom request handler. They return empty
// responses with controllable response headers.
const char kImagePath[] = "/handled-by-test/image.png";
const char kImagePath2[] = "/handled-by-test/image2.png";
const char kStylePath[] = "/handled-by-test/style.css";
const char kStylePath2[] = "/handled-by-test/style2.css";
const char kScriptPath[] = "/handled-by-test/script.js";
const char kScriptPath2[] = "/handled-by-test/script2.js";
const char kFontPath[] = "/handled-by-test/font.ttf";
const char kRedirectPath[] = "/handled-by-test/redirect.html";
const char kRedirectPath2[] = "/handled-by-test/redirect2.html";
const char kRedirectPath3[] = "/handled-by-test/redirect3.html";
// These are loaded from a file by the test server.
const char kHtmlSubresourcesPath[] = "/predictors/html_subresources.html";
const char kHtmlDocumentWritePath[] = "/predictors/document_write.html";
const char kScriptDocumentWritePath[] = "/predictors/document_write.js";
const char kHtmlAppendChildPath[] = "/predictors/append_child.html";
const char kScriptAppendChildPath[] = "/predictors/append_child.js";
const char kHtmlInnerHtmlPath[] = "/predictors/inner_html.html";
const char kScriptInnerHtmlPath[] = "/predictors/inner_html.js";
const char kHtmlXHRPath[] = "/predictors/xhr.html";
const char kScriptXHRPath[] = "/predictors/xhr.js";
const char kHtmlIframePath[] = "/predictors/html_iframe.html";
const char kHtmlJavascriptRedirectPath[] =
"/predictors/javascript_redirect.html";
const char kHtmlFcpOrderPath[] = "/predictors/subresource_fcp_order.html";
// Host, path.
const char* kScript[2] = {kFooHost, kScriptPath};
const char* kImage[2] = {kBarHost, kImagePath};
const char* kFont[2] = {kFooHost, kFontPath};
const char* kStyle[2] = {kBazHost, kStylePath};
struct ResourceSummary {
ResourceSummary()
: version(0),
is_no_store(false),
is_external(false),
is_observable(true),
is_prohibited(false) {
request.before_first_contentful_paint = true;
}
URLRequestSummary request;
// Allows to update HTTP ETag.
size_t version;
// True iff "Cache-control: no-store" header is present.
bool is_no_store;
// True iff a request for this resource must be ignored by the custom handler.
bool is_external;
// True iff the LearningObserver must observe this resource.
bool is_observable;
// A request with |is_prohibited| set to true makes the test that originates
// the request fail.
bool is_prohibited;
// If set, the HTTP request handler will sleep for this long before
// responding.
base::TimeDelta delay;
};
struct RedirectEdge {
// This response code should be returned by previous url in the chain.
net::HttpStatusCode code;
GURL url;
bool is_client_side;
};
// Helper class to track and allow waiting for ResourcePrefetchPredictor
// initialization. WARNING: OnPredictorInitialized event will not be fired if
// ResourcePrefetchPredictor is initialized before the observer creation.
class InitializationObserver : public TestObserver {
public:
explicit InitializationObserver(ResourcePrefetchPredictor* predictor)
: TestObserver(predictor) {}
void OnPredictorInitialized() override { run_loop_.Quit(); }
void Wait() { run_loop_.Run(); }
private:
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(InitializationObserver);
};
class BrowsingDataRemoverObserver
: public content::BrowsingDataRemover::Observer {
public:
explicit BrowsingDataRemoverObserver(content::BrowsingDataRemover* remover)
: remover_(remover) {
remover_->AddObserver(this);
}
~BrowsingDataRemoverObserver() override { remover_->RemoveObserver(this); }
void OnBrowsingDataRemoverDone() override { run_loop_.Quit(); }
void Wait() { run_loop_.Run(); }
private:
content::BrowsingDataRemover* remover_;
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(BrowsingDataRemoverObserver);
};
void RemoveDuplicateSubresources(std::vector<URLRequestSummary>* subresources) {
std::stable_sort(subresources->begin(), subresources->end(),
[](const URLRequestSummary& x, const URLRequestSummary& y) {
return x.resource_url < y.resource_url;
});
subresources->erase(
std::unique(subresources->begin(), subresources->end(),
[](const URLRequestSummary& x, const URLRequestSummary& y) {
return x.resource_url == y.resource_url;
}),
subresources->end());
}
// Fill a NavigationID with "empty" data that does not trigger
// the is_valid DCHECK(). Allows comparing.
void SetValidNavigationID(NavigationID* navigation_id) {
navigation_id->tab_id = 0;
navigation_id->main_frame_url = GURL("http://127.0.0.1");
}
void ModifySubresourceForComparison(URLRequestSummary* subresource,
bool match_navigation_id,
bool match_before_first_contentful_paint) {
if (!match_navigation_id)
SetValidNavigationID(&subresource->navigation_id);
if (!match_before_first_contentful_paint)
subresource->before_first_contentful_paint = true;
if (subresource->resource_type == content::RESOURCE_TYPE_IMAGE &&
subresource->priority == net::LOWEST) {
// Fuzzy comparison for images because an image priority can be
// boosted during layout via
// ResourceFetcher::updateAllImageResourcePriorities().
subresource->priority = net::MEDIUM;
}
}
// Does a custom comparison of subresources of URLRequestSummary
// and fail the test if the expectation is not met.
void CompareSubresources(std::vector<URLRequestSummary> actual_subresources,
std::vector<URLRequestSummary> expected_subresources,
bool match_navigation_id,
bool match_before_first_contentful_paint) {
// Duplicate resources can be observed in a single navigation but
// ResourcePrefetchPredictor only cares about the first occurrence of each.
RemoveDuplicateSubresources(&actual_subresources);
for (auto& subresource : actual_subresources)
ModifySubresourceForComparison(&subresource, match_navigation_id,
match_before_first_contentful_paint);
for (auto& subresource : expected_subresources)
ModifySubresourceForComparison(&subresource, match_navigation_id,
match_before_first_contentful_paint);
EXPECT_THAT(actual_subresources,
testing::UnorderedElementsAreArray(expected_subresources));
}
std::string CreateVersionedETag(size_t version, const std::string& path) {
return base::StringPrintf("'%zu%s'", version, path.c_str());
}
GURL GetRequestURL(const net::test_server::HttpRequest& request) {
GURL resource_url = request.GetURL();
// Retrieve the host that was used in the request because
// resource_url contains a resolved host (e.g. 127.0.0.1).
if (request.headers.find("Host") != request.headers.end()) {
auto host_port_pair =
net::HostPortPair::FromString(request.headers.at("Host"));
GURL::Replacements replace_host;
replace_host.SetHostStr(host_port_pair.host());
resource_url = resource_url.ReplaceComponents(replace_host);
} else {
ADD_FAILURE() << "Host header was not found in a HttpRequest to url: "
<< resource_url.spec();
}
return resource_url;
}
} // namespace
// Helper class to track and allow waiting for a single OnNavigationLearned
// event. The information provided by this event is also used to verify that
// ResourcePrefetchPredictor works as expected.
class LearningObserver : public TestObserver {
public:
LearningObserver(ResourcePrefetchPredictor* predictor,
const size_t expected_url_visit_count,
const PageRequestSummary& expected_summary,
bool match_navigation_id,
bool match_before_first_contentful_paint)
: TestObserver(predictor),
url_visit_count_(expected_url_visit_count),
summary_(expected_summary),
match_navigation_id_(match_navigation_id),
match_before_first_contentful_paint_(
match_before_first_contentful_paint) {}
// TestObserver:
void OnNavigationLearned(size_t url_visit_count,
const PageRequestSummary& summary) override {
EXPECT_EQ(url_visit_count, url_visit_count_);
EXPECT_EQ(summary.main_frame_url, summary_.main_frame_url);
EXPECT_EQ(summary.initial_url, summary_.initial_url);
for (const auto& resource : summary.subresource_requests)
current_navigation_ids_.insert(resource.navigation_id);
CompareSubresources(summary.subresource_requests,
summary_.subresource_requests, match_navigation_id_,
match_before_first_contentful_paint_);
run_loop_.Quit();
}
void Wait() { run_loop_.Run(); }
std::set<NavigationID>& current_navigation_ids() {
return current_navigation_ids_;
}
private:
base::RunLoop run_loop_;
size_t url_visit_count_;
PageRequestSummary summary_;
bool match_navigation_id_;
bool match_before_first_contentful_paint_;
std::set<NavigationID> current_navigation_ids_;
DISALLOW_COPY_AND_ASSIGN(LearningObserver);
};
// Helper class to track and allow waiting for a single OnPrefetchingFinished
// event. Checks also that {Start,Stop}Prefetching are called with the right
// argument.
class PrefetchingObserver : public TestLoadingObserver {
public:
PrefetchingObserver(LoadingPredictor* predictor,
const GURL& expected_main_frame_url)
: TestLoadingObserver(predictor),
main_frame_url_(expected_main_frame_url) {}
// LoadingTestObserver:
void OnPrefetchingStarted(const GURL& main_frame_url) override {
EXPECT_EQ(main_frame_url_, main_frame_url);
}
void OnPrefetchingStopped(const GURL& main_frame_url) override {
EXPECT_EQ(main_frame_url_, main_frame_url);
}
void OnPrefetchingFinished(const GURL& main_frame_url) override {
EXPECT_EQ(main_frame_url_, main_frame_url);
run_loop_.Quit();
}
// TODO(alexilin): Consider checking that prefetching does not activate
// learning here.
void Wait() { run_loop_.Run(); }
private:
base::RunLoop run_loop_;
GURL main_frame_url_;
DISALLOW_COPY_AND_ASSIGN(PrefetchingObserver);
};
class ResourcePrefetchPredictorBrowserTest : public InProcessBrowserTest {
protected:
using URLRequestSummary = URLRequestSummary;
ResourcePrefetchPredictorBrowserTest() {}
~ResourcePrefetchPredictorBrowserTest() override {}
void SetUp() override {
std::map<std::string, std::string> parameters = {
{kModeParamName, kExternalPrefetchingMode},
{kEnableUrlLearningParamName, "true"}};
scoped_feature_list_.InitAndEnableFeatureWithParameters(
predictors::kSpeculativeResourcePrefetchingFeature, parameters);
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
// Resolving all hosts to local allows us to have
// cross domains navigations (matching url_visit_count_, etc).
host_resolver()->AddRule("*", "127.0.0.1");
https_server_ = base::MakeUnique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
for (auto* server : {embedded_test_server(), https_server()}) {
server->AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
server->RegisterRequestHandler(base::Bind(
&ResourcePrefetchPredictorBrowserTest::HandleRedirectRequest,
base::Unretained(this)));
server->RegisterRequestHandler(base::Bind(
&ResourcePrefetchPredictorBrowserTest::HandleResourceRequest,
base::Unretained(this)));
server->RegisterRequestMonitor(base::Bind(
&ResourcePrefetchPredictorBrowserTest::MonitorResourceRequest,
base::Unretained(this)));
ASSERT_TRUE(server->Start());
}
predictor_ = LoadingPredictorFactory::GetForProfile(browser()->profile());
ASSERT_TRUE(predictor_);
resource_prefetch_predictor_ = predictor_->resource_prefetch_predictor();
// URLs from the test server contain a port number.
LoadingDataCollector::SetAllowPortInUrlsForTesting(true);
EnsurePredictorInitialized();
histogram_tester_.reset(new base::HistogramTester());
}
void TearDownOnMainThread() override {
LoadingDataCollector::SetAllowPortInUrlsForTesting(false);
}
void TestLearningAndPrefetching(
const GURL& main_frame_url,
bool match_before_first_contentful_paint = false) {
// Navigate to |main_frame_url| and check all the expectations.
NavigateToURLAndCheckSubresources(main_frame_url,
WindowOpenDisposition::CURRENT_TAB,
match_before_first_contentful_paint);
ClearCache();
// It is needed to have at least two resource hits to trigger prefetch.
NavigateToURLAndCheckSubresources(main_frame_url,
WindowOpenDisposition::CURRENT_TAB,
match_before_first_contentful_paint);
ClearCache();
// Prefetch all needed resources and change expectations so that all
// cacheable resources should be served from cache next navigation.
PrefetchURL(main_frame_url);
// To be sure that the browser send no requests to the server after
// prefetching.
NavigateToURLAndCheckSubresourcesAllCached(main_frame_url);
}
void NavigateToURLAndCheckSubresourcesAllCached(const GURL& navigation_url) {
for (auto& kv : resources_) {
if (kv.second.is_observable)
kv.second.is_prohibited = true;
}
NavigateToURLAndCheckSubresources(navigation_url);
for (auto& kv : resources_) {
if (kv.second.is_observable)
kv.second.is_prohibited = false;
}
}
void NavigateToURLAndCheckSubresources(
const GURL& navigation_url,
WindowOpenDisposition disposition = WindowOpenDisposition::CURRENT_TAB,
bool match_before_first_contentful_paint = false) {
GURL initial_url = GetLastClientSideRedirectEndpoint(navigation_url);
GURL main_frame_url = GetRedirectEndpoint(navigation_url);
std::vector<URLRequestSummary> url_request_summaries;
for (const auto& kv : resources_) {
if (kv.second.is_observable) {
url_request_summaries.push_back(
GetURLRequestSummaryForResource(main_frame_url, kv.second));
}
}
bool match_navigation_id =
disposition == WindowOpenDisposition::CURRENT_TAB;
LearningObserver observer(
resource_prefetch_predictor_, UpdateAndGetVisitCount(initial_url),
CreatePageRequestSummary(main_frame_url.spec(), initial_url.spec(),
url_request_summaries),
match_navigation_id, match_before_first_contentful_paint);
ui_test_utils::NavigateToURLWithDisposition(
browser(), navigation_url, disposition,
ui_test_utils::BROWSER_TEST_NONE);
observer.Wait();
for (auto& kv : resources_) {
if (kv.second.is_observable)
kv.second.request.was_cached = true;
}
for (const auto& nav : observer.current_navigation_ids())
navigation_id_history_.insert(nav);
}
void NavigateToURLAndCheckPrefetching(const GURL& main_frame_url) {
PrefetchingObserver observer(predictor_, main_frame_url);
ui_test_utils::NavigateToURL(browser(), main_frame_url);
observer.Wait();
for (auto& kv : resources_) {
if (kv.second.is_observable)
kv.second.request.was_cached = true;
}
}
void PrefetchURL(const GURL& main_frame_url) {
PrefetchingObserver observer(predictor_, main_frame_url);
predictor_->PrepareForPageLoad(main_frame_url, HintOrigin::EXTERNAL);
observer.Wait();
for (auto& kv : resources_) {
if (kv.second.is_observable)
kv.second.request.was_cached = true;
}
}
void TryToPrefetchURL(const GURL& main_frame_url) {
predictor_->PrepareForPageLoad(main_frame_url, HintOrigin::EXTERNAL);
}
ResourceSummary* AddResource(const GURL& resource_url,
content::ResourceType resource_type,
net::RequestPriority priority) {
auto pair_and_whether_inserted =
resources_.insert(std::make_pair(resource_url, ResourceSummary()));
EXPECT_TRUE(pair_and_whether_inserted.second) << resource_url
<< " was inserted twice";
ResourceSummary* resource = &pair_and_whether_inserted.first->second;
resource->request.resource_url = resource_url;
resource->request.resource_type = resource_type;
resource->request.priority = priority;
resource->request.has_validators = true;
resource->request.request_url = resource_url;
return resource;
}
ResourceSummary* AddExternalResource(const GURL& resource_url,
content::ResourceType resource_type,
net::RequestPriority priority) {
auto* resource = AddResource(resource_url, resource_type, priority);
resource->is_external = true;
return resource;
}
void AddUnobservableResources(const std::vector<GURL>& resource_urls) {
for (const GURL& resource_url : resource_urls) {
auto* resource =
AddResource(resource_url, content::RESOURCE_TYPE_SUB_RESOURCE,
net::DEFAULT_PRIORITY);
resource->is_observable = false;
}
}
void AddRedirectChain(const GURL& initial_url,
const std::vector<RedirectEdge>& redirect_chain) {
ASSERT_FALSE(redirect_chain.empty());
GURL current = initial_url;
for (const auto& edge : redirect_chain) {
auto result = redirects_.insert(std::make_pair(current, edge));
EXPECT_TRUE(result.second) << current << " already has a redirect.";
current = edge.url;
}
}
void ClearResources() { resources_.clear(); }
void ClearCache() {
content::BrowsingDataRemover* remover =
content::BrowserContext::GetBrowsingDataRemover(browser()->profile());
BrowsingDataRemoverObserver observer(remover);
remover->RemoveAndReply(
base::Time(), base::Time::Max(),
content::BrowsingDataRemover::DATA_TYPE_CACHE,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, &observer);
observer.Wait();
for (auto& kv : resources_)
kv.second.request.was_cached = false;
}
// Shortcuts for convenience.
GURL GetURL(const std::string& path) const {
return embedded_test_server()->GetURL(path);
}
GURL GetURLWithHost(const std::string& host,
const std::string& path,
bool https = false) const {
auto* server = https ? https_server() : embedded_test_server();
return server->GetURL(host, path);
}
// Only works if the resulting URL is served from a file.
std::string GetPathWithReplacements(const std::string& path) const {
std::string port = base::StringPrintf("%d", embedded_test_server()->port());
std::string path_with_replacements;
net::test_server::GetFilePathWithReplacements(
path, {{"REPLACE_WITH_PORT", port}}, &path_with_replacements);
return path_with_replacements;
}
GURL GetPageURLWithReplacements(const std::string& host,
const std::string& path,
bool https = false) const {
std::string path_with_replacements = GetPathWithReplacements(path);
return GetURLWithHost(host, path_with_replacements, https);
}
// Returns the embedded test server working over HTTPS.
const net::EmbeddedTestServer* https_server() const {
return https_server_.get();
}
net::EmbeddedTestServer* https_server() { return https_server_.get(); }
size_t navigation_ids_history_size() const {
return navigation_id_history_.size();
}
// Expects the resources from kHtmlSubresourcesPath.
std::vector<ResourceSummary*> AddResourcesFromSubresourceHtml() {
// These resources have default priorities that correspond to
// blink::TypeToPriority().
return {AddResource(GetURLWithHost(kImage[0], kImage[1]),
content::RESOURCE_TYPE_IMAGE, net::LOWEST),
AddResource(GetURLWithHost(kStyle[0], kStyle[1]),
content::RESOURCE_TYPE_STYLESHEET, net::HIGHEST),
AddResource(GetURLWithHost(kScript[0], kScript[1]),
content::RESOURCE_TYPE_SCRIPT, net::MEDIUM),
AddResource(GetURLWithHost(kFont[0], kFont[1]),
content::RESOURCE_TYPE_FONT_RESOURCE, net::HIGHEST)};
}
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<base::HistogramTester> histogram_tester_;
private:
// ResourcePrefetchPredictor needs to be initialized before the navigation
// happens otherwise this navigation will be ignored by predictor.
void EnsurePredictorInitialized() {
if (resource_prefetch_predictor_->initialization_state_ ==
ResourcePrefetchPredictor::INITIALIZED) {
return;
}
InitializationObserver observer(resource_prefetch_predictor_);
if (resource_prefetch_predictor_->initialization_state_ ==
ResourcePrefetchPredictor::NOT_INITIALIZED) {
resource_prefetch_predictor_->StartInitialization();
}
observer.Wait();
}
URLRequestSummary GetURLRequestSummaryForResource(
const GURL& main_frame_url,
const ResourceSummary& resource_summary) const {
URLRequestSummary summary(resource_summary.request);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
summary.navigation_id =
NavigationID(web_contents, main_frame_url, base::TimeTicks::Now());
return summary;
}
GURL GetRedirectEndpoint(const GURL& initial_url) const {
GURL current = initial_url;
while (true) {
std::map<GURL, RedirectEdge>::const_iterator it =
redirects_.find(current);
if (it == redirects_.end())
break;
current = it->second.url;
}
return current;
}
GURL GetLastClientSideRedirectEndpoint(const GURL& initial_url) const {
GURL last_client_side_redirect_url = initial_url;
GURL current = initial_url;
while (true) {
std::map<GURL, RedirectEdge>::const_iterator it =
redirects_.find(current);
if (it == redirects_.end())
break;
current = it->second.url;
if (it->second.is_client_side)
last_client_side_redirect_url = current;
}
return last_client_side_redirect_url;
}
void MonitorResourceRequest(
const net::test_server::HttpRequest& request) const {
std::map<GURL, ResourceSummary>::const_iterator resource_it =
resources_.find(request.GetURL());
if (resource_it == resources_.end())
return;
const ResourceSummary& summary = resource_it->second;
EXPECT_FALSE(summary.is_prohibited) << request.GetURL() << "\n"
<< request.all_headers;
}
// The custom handler for resource requests from the browser to an
// EmbeddedTestServer. Runs on the EmbeddedTestServer IO thread.
// Finds the data to serve requests in |resources_| map keyed by a request
// URL.
// Uses also the following headers from the |request|:
// - "Host" to retrieve the host that actually was issued by the browser.
// - "If-None-Match" to determine if the resource is still valid. If the
// ETag values match, the handler responds with a HTTP 304 status.
std::unique_ptr<net::test_server::HttpResponse> HandleResourceRequest(
const net::test_server::HttpRequest& request) const {
std::map<GURL, ResourceSummary>::const_iterator resource_it =
resources_.find(GetRequestURL(request));
if (resource_it == resources_.end())
return nullptr;
const ResourceSummary& summary = resource_it->second;
if (summary.is_external)
return nullptr;
auto http_response =
base::MakeUnique<net::test_server::BasicHttpResponse>();
if (request.headers.find("If-None-Match") != request.headers.end() &&
request.headers.at("If-None-Match") ==
CreateVersionedETag(summary.version, request.relative_url)) {
http_response->set_code(net::HTTP_NOT_MODIFIED);
} else {
http_response->set_code(net::HTTP_OK);
}
if (!summary.request.mime_type.empty())
http_response->set_content_type(summary.request.mime_type);
if (summary.is_no_store)
http_response->AddCustomHeader("Cache-Control", "no-store");
if (summary.request.has_validators) {
http_response->AddCustomHeader(
"ETag", CreateVersionedETag(summary.version, request.relative_url));
}
if (summary.request.always_revalidate)
http_response->AddCustomHeader("Cache-Control", "no-cache");
else
http_response->AddCustomHeader("Cache-Control", "max-age=2147483648");
// Add some content, otherwise the prefetch size histogram rounds down to
// 0kB.
http_response->set_content(std::string(1024, ' '));
if (!summary.delay.is_zero())
base::PlatformThread::Sleep(summary.delay);
return std::move(http_response);
}
// The custom handler for redirect requests from the browser to an
// EmbeddedTestServer. Running on the EmbeddedTestServer IO thread.
// Finds the data to serve requests in |redirects_| map keyed by a request
// URL.
std::unique_ptr<net::test_server::HttpResponse> HandleRedirectRequest(
const net::test_server::HttpRequest& request) const {
std::map<GURL, RedirectEdge>::const_iterator redirect_it =
redirects_.find(GetRequestURL(request));
if (redirect_it == redirects_.end() || redirect_it->second.is_client_side)
return nullptr;
auto http_response =
base::MakeUnique<net::test_server::BasicHttpResponse>();
http_response->set_code(redirect_it->second.code);
http_response->AddCustomHeader("Location", redirect_it->second.url.spec());
return std::move(http_response);
}
size_t UpdateAndGetVisitCount(const GURL& main_frame_url) {
return ++visit_count_[main_frame_url];
}
LoadingPredictor* predictor_;
ResourcePrefetchPredictor* resource_prefetch_predictor_;
std::unique_ptr<net::EmbeddedTestServer> https_server_;
std::map<GURL, ResourceSummary> resources_;
std::map<GURL, RedirectEdge> redirects_;
std::map<GURL, size_t> visit_count_;
std::set<NavigationID> navigation_id_history_;
DISALLOW_COPY_AND_ASSIGN(ResourcePrefetchPredictorBrowserTest);
};
// Subclass to test HintOrigin::NAVIGATION.
class ResourcePrefetchPredictorPrefetchingBrowserTest
: public ResourcePrefetchPredictorBrowserTest {
public:
ResourcePrefetchPredictorPrefetchingBrowserTest() {}
~ResourcePrefetchPredictorPrefetchingBrowserTest() override {}
void SetUp() override {
std::map<std::string, std::string> parameters = {
{kModeParamName, kPrefetchingMode},
{kEnableUrlLearningParamName, "true"}};
scoped_feature_list_.InitAndEnableFeatureWithParameters(
predictors::kSpeculativeResourcePrefetchingFeature, parameters);
InProcessBrowserTest::SetUp();
}
private:
DISALLOW_COPY_AND_ASSIGN(ResourcePrefetchPredictorPrefetchingBrowserTest);
};
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest, Simple) {
AddResourcesFromSubresourceHtml();
GURL url = GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
TestLearningAndPrefetching(url);
// The local cache is cleared.
histogram_tester_->ExpectBucketCount(
internal::kResourcePrefetchPredictorPrefetchMissesCountCached, 0, 1);
histogram_tester_->ExpectBucketCount(
internal::kResourcePrefetchPredictorPrefetchMissesCountNotCached, 0, 1);
histogram_tester_->ExpectBucketCount(
internal::kResourcePrefetchPredictorPrefetchHitsCountCached, 0, 1);
histogram_tester_->ExpectBucketCount(
internal::kResourcePrefetchPredictorPrefetchHitsCountNotCached, 4, 1);
histogram_tester_->ExpectBucketCount(
internal::kResourcePrefetchPredictorPrefetchMissesSize, 0, 1);
// Each request is ~1k, see HandleResourceRequest() above.
histogram_tester_->ExpectBucketCount(
internal::kResourcePrefetchPredictorPrefetchHitsSize, 4, 1);
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
SubresourceFcpOrder) {
AddResource(GetURL(kStylePath), content::RESOURCE_TYPE_STYLESHEET,
net::HIGHEST);
AddResource(GetURL(kScriptPath), content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
ResourceSummary* image = AddResource(
GetURL(kImagePath), content::RESOURCE_TYPE_IMAGE, net::LOWEST);
// Delay HTTP response to ensure enough time to receive notice of
// firstContentfulPaint.
image->delay = base::TimeDelta::FromMilliseconds(2500);
image->request.before_first_contentful_paint = false;
TestLearningAndPrefetching(GetURL(kHtmlFcpOrderPath), true);
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest, Redirect) {
GURL initial_url = GetURLWithHost(kFooHost, kRedirectPath);
GURL redirected_url =
GetPageURLWithReplacements(kBarHost, kHtmlSubresourcesPath);
AddRedirectChain(initial_url,
{{net::HTTP_MOVED_PERMANENTLY, redirected_url}});
AddResourcesFromSubresourceHtml();
TestLearningAndPrefetching(initial_url);
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest, RedirectChain) {
GURL initial_url = GetURLWithHost(kFooHost, kRedirectPath);
GURL redirected_url =
GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
AddRedirectChain(
initial_url,
{{net::HTTP_FOUND, GetURLWithHost(kBarHost, kRedirectPath2)},
{net::HTTP_MOVED_PERMANENTLY, GetURLWithHost(kBazHost, kRedirectPath3)},
{net::HTTP_FOUND, redirected_url}});
AddResourcesFromSubresourceHtml();
TestLearningAndPrefetching(initial_url);
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
HttpToHttpsRedirect) {
GURL initial_url = GetURLWithHost(kFooHost, kRedirectPath);
GURL redirected_url =
GetPageURLWithReplacements(kLocalHost, kHtmlSubresourcesPath, true);
// Only one resource, as HTTP images in HTTPS pages are only a warning, not
// an error.
AddResource(GetURLWithHost(kImage[0], kImage[1]),
content::RESOURCE_TYPE_IMAGE, net::LOWEST);
AddRedirectChain(initial_url,
{{net::HTTP_MOVED_PERMANENTLY, redirected_url}});
TestLearningAndPrefetching(initial_url);
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
JavascriptDocumentWrite) {
auto* externalScript =
AddExternalResource(GetURL(kScriptDocumentWritePath),
content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
externalScript->request.mime_type = kJavascriptMime;
AddResource(GetURL(kImagePath), content::RESOURCE_TYPE_IMAGE, net::LOWEST);
AddResource(GetURL(kStylePath), content::RESOURCE_TYPE_STYLESHEET,
net::HIGHEST);
AddResource(GetURL(kScriptPath), content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
TestLearningAndPrefetching(GetURL(kHtmlDocumentWritePath));
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
JavascriptAppendChild) {
auto* externalScript =
AddExternalResource(GetURL(kScriptAppendChildPath),
content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
externalScript->request.mime_type = kJavascriptMime;
AddResource(GetURL(kImagePath), content::RESOURCE_TYPE_IMAGE, net::LOWEST);
AddResource(GetURL(kStylePath), content::RESOURCE_TYPE_STYLESHEET,
net::HIGHEST);
// This script has net::LOWEST priority because it's executed asynchronously.
AddResource(GetURL(kScriptPath), content::RESOURCE_TYPE_SCRIPT, net::LOWEST);
TestLearningAndPrefetching(GetURL(kHtmlAppendChildPath));
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
JavascriptInnerHtml) {
auto* externalScript = AddExternalResource(
GetURL(kScriptInnerHtmlPath), content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
externalScript->request.mime_type = kJavascriptMime;
AddResource(GetURL(kImagePath), content::RESOURCE_TYPE_IMAGE, net::LOWEST);
AddResource(GetURL(kStylePath), content::RESOURCE_TYPE_STYLESHEET,
net::HIGHEST);
// https://www.w3.org/TR/2014/REC-html5-20141028/scripting-1.html#the-script-element
// Script elements don't execute when inserted using innerHTML attribute.
AddUnobservableResources({GetURL(kScriptPath)});
TestLearningAndPrefetching(GetURL(kHtmlInnerHtmlPath));
}
// Requests originated by XMLHttpRequest have content::RESOURCE_TYPE_XHR.
// Actual resource type is inferred from the mime-type.
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest, JavascriptXHR) {
auto* externalScript = AddExternalResource(
GetURL(kScriptXHRPath), content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
externalScript->request.mime_type = kJavascriptMime;
auto* image = AddResource(GetURL(kImagePath), content::RESOURCE_TYPE_IMAGE,
net::HIGHEST);
image->request.mime_type = kImageMime;
auto* style = AddResource(GetURL(kStylePath),
content::RESOURCE_TYPE_STYLESHEET, net::HIGHEST);
style->request.mime_type = kStyleMime;
auto* script = AddResource(GetURL(kScriptPath), content::RESOURCE_TYPE_SCRIPT,
net::HIGHEST);
script->request.mime_type = kJavascriptMime;
TestLearningAndPrefetching(GetURL(kHtmlXHRPath));
}
// ResourcePrefetchPredictor ignores all resources requested from subframes.
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
IframeShouldBeIgnored) {
// Included from html_iframe.html.
AddResource(GetURL(kImagePath2), content::RESOURCE_TYPE_IMAGE, net::LOWEST);
AddResource(GetURL(kStylePath2), content::RESOURCE_TYPE_STYLESHEET,
net::HIGHEST);
AddResource(GetURL(kScriptPath2), content::RESOURCE_TYPE_SCRIPT, net::MEDIUM);
// Included from <iframe src="html_subresources.html"> and shouldn't be
// observed.
AddUnobservableResources({GetURL(kImagePath), GetURL(kStylePath),
GetURL(kScriptPath), GetURL(kFontPath)});
TestLearningAndPrefetching(GetURL(kHtmlIframePath));
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
CrossSiteNavigation) {
AddResourcesFromSubresourceHtml();
GURL url = GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
TestLearningAndPrefetching(url);
ClearResources();
ClearCache();
AddResourcesFromSubresourceHtml();
// Navigating to kBarHost, although done in the same tab, will generate a new
// process.
url = GetPageURLWithReplacements(kBarHost, kHtmlSubresourcesPath);
TestLearningAndPrefetching(url);
}
// In this test we are trying to assess if :
// - Reloading in the same tab is using the same NavigationID.
// - Navigation into a new tab, window or popup yields different NavigationID's.
// - Navigating twice to a new browser/popup yields different NavigationID's.
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
TabIdBehavingAsExpected) {
AddResourcesFromSubresourceHtml();
GURL url = GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
NavigateToURLAndCheckSubresources(url);
EXPECT_EQ(navigation_ids_history_size(), 1U);
ClearCache();
NavigateToURLAndCheckSubresources(url);
EXPECT_EQ(navigation_ids_history_size(), 1U);
ClearCache();
NavigateToURLAndCheckSubresources(url,
WindowOpenDisposition::NEW_BACKGROUND_TAB);
EXPECT_EQ(navigation_ids_history_size(), 2U);
ClearCache();
NavigateToURLAndCheckSubresources(url, WindowOpenDisposition::NEW_WINDOW);
EXPECT_EQ(navigation_ids_history_size(), 3U);
ClearCache();
NavigateToURLAndCheckSubresources(url, WindowOpenDisposition::NEW_POPUP);
EXPECT_EQ(navigation_ids_history_size(), 4U);
}
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest, AlwaysRevalidate) {
auto resources = AddResourcesFromSubresourceHtml();
GURL url = GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
for (auto* resource : resources)
resource->request.always_revalidate = true;
TestLearningAndPrefetching(url);
}
// Client-side redirects currently aren't tracked by ResourcePrefetchPredictor.
// A client-side redirect initiates a new navigation to the redirect destination
// URL and aborts the current navigation so that the OnLoad event is not fired.
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorBrowserTest,
JavascriptRedirectsAreNotHandled) {
GURL redirected_url =
GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
GURL initial_url = GetURLWithHost(kBarHost, kHtmlJavascriptRedirectPath);
initial_url =
net::AppendQueryParameter(initial_url, "url", redirected_url.spec());
AddRedirectChain(initial_url,
{{net::HTTP_TEMPORARY_REDIRECT, redirected_url, true}});
AddResourcesFromSubresourceHtml();
// Two navigations will occur. LearningObserver will get events only for the
// second navigation because the first one will be aborted.
NavigateToURLAndCheckSubresources(initial_url);
ClearCache();
// It is needed to have at least two resource hits to trigger prefetch.
NavigateToURLAndCheckSubresources(initial_url);
ClearCache();
// Prefetching of |initial_url| has no effect because there is no entry in
// the predictor database corresponding the client-side redirect.
TryToPrefetchURL(initial_url);
NavigateToURLAndCheckSubresources(initial_url);
ClearCache();
// But the predictor database contains all subresources for the endpoint url
// so this prefetch works.
PrefetchURL(redirected_url);
NavigateToURLAndCheckSubresourcesAllCached(redirected_url);
}
// Makes sure that {Stop,Start}Prefetching are called with the same argument.
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorPrefetchingBrowserTest,
Simple) {
AddResourcesFromSubresourceHtml();
GURL main_frame_url =
GetPageURLWithReplacements(kFooHost, kHtmlSubresourcesPath);
NavigateToURLAndCheckSubresources(main_frame_url);
ClearCache();
NavigateToURLAndCheckSubresources(main_frame_url);
ClearCache();
NavigateToURLAndCheckPrefetching(main_frame_url);
}
// Makes sure that {Stop,Start}Prefetching are called with the same argument in
// presence of redirect.
IN_PROC_BROWSER_TEST_F(ResourcePrefetchPredictorPrefetchingBrowserTest,
Redirect) {
GURL initial_url = GetURLWithHost(kFooHost, kRedirectPath);
GURL redirected_url =
GetPageURLWithReplacements(kBarHost, kHtmlSubresourcesPath);
AddRedirectChain(initial_url,
{{net::HTTP_MOVED_PERMANENTLY, redirected_url}});
AddResourcesFromSubresourceHtml();
NavigateToURLAndCheckSubresources(initial_url);
ClearCache();
NavigateToURLAndCheckSubresources(initial_url);
ClearCache();
NavigateToURLAndCheckPrefetching(initial_url);
}
} // namespace predictors