blob: c06245a4cf0fff1292927482032af5c02ddcbc67 [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 "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "content/browser/code_cache/generated_code_cache.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
namespace content {
namespace {
bool SupportsSharedWorker() {
#if BUILDFLAG(IS_ANDROID)
// SharedWorkers are not enabled on Android. https://crbug.com/154571
return false;
#else
return true;
#endif
}
} // namespace
enum class CodeCacheTestCase {
kCachePartitioningEnabled,
kCachePartitioningDisabled,
};
class CodeCacheBrowserTest
: public ContentBrowserTest,
public testing::WithParamInterface<CodeCacheTestCase> {
public:
CodeCacheBrowserTest() {
// Enable the split HTTP cache since the GeneratedCodeCache won't consider
// partitioning by NIK unless the HTTP cache does.
feature_split_cache_by_network_isolation_key_.InitAndEnableFeature(
net::features::kSplitCacheByNetworkIsolationKey);
// Enable third-party storage partitioning so that for the Shared Worker
// test, different Shared Workers are used in the third-party contexts with
// differing top-level sites.
feature_third_party_storage_partitioning_.InitAndEnableFeature(
net::features::kThirdPartyStoragePartitioning);
feature_split_code_cache_by_network_isolation_key_.InitWithFeatureState(
net::features::kSplitCodeCacheByNetworkIsolationKey,
IsCachePartitioningEnabled());
}
bool IsCachePartitioningEnabled() const {
return GetParam() == CodeCacheTestCase::kCachePartitioningEnabled;
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&CodeCacheBrowserTest::CachedScriptHandler, base::Unretained(this)));
ASSERT_TRUE(embedded_test_server()->Start());
}
std::unique_ptr<net::test_server::HttpResponse> CachedScriptHandler(
const net::test_server::HttpRequest& request) {
GURL absolute_url = embedded_test_server()->GetURL(request.relative_url);
// Worker scripts will fetch this once the cacheable resource has been
// loaded and the test logic (checking histograms) can continue.
if (absolute_url.path() == "/done.js") {
content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE,
std::move(done_callback_));
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content("//done!");
http_response->set_content_type("application/javascript");
return http_response;
}
// Returns a JavaScript file that should be cacheable by the
// GeneratedCodeCache (>1024 characters).
if (absolute_url.path() == "/cacheable.js") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
std::string content = "let variable = 'hello!';\n";
// Make sure the script is long enough to be eligible for caching.
for (int i = 0; i < 16; i++) {
content += std::string(64, '/') + '\n';
}
http_response->set_content(content);
http_response->set_content_type("application/javascript");
// NOTE: It seems that if we set a Cache-Control header (even
// 'Cache-Control: max-age=100000'), this script no longer gets cached in
// the GeneratedCodeCache, so be sure not to set one here.
return http_response;
}
// Returns an HTML file that will load /cacheable.js.
if (absolute_url.path() == "/cacheable.html") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
std::string content =
"<html><head><title>Title</title></head>"
"<script src='/cacheable.js'></script></html>";
http_response->set_content(content);
return http_response;
}
// Returns a JavaScript file that should itself be eligible for caching in
// the GeneratedCodeCache and that will load /cacheable.js via
// importScripts.
if (absolute_url.path() == "/worker.js") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
// Self-terminate the worker just after loading the scripts so that the
// parent context doesn't need to wait for the worker's termination when
// cleaning up the test.
std::string content = base::StringPrintf(
"importScripts('cacheable.js', 'done.js');"
"close();");
for (int i = 0; i < 16; i++) {
content += std::string(64, '/') + '\n';
}
http_response->set_content(content);
http_response->set_content_type("application/javascript");
return http_response;
}
// Return a page that will create a Shared Worker that uses /worker.js.
if (absolute_url.path() == "/shared-worker.html") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
std::string content =
"<html><head><title>Title</title></head>"
"<script>let w = new SharedWorker('worker.js');</script></html>";
http_response->set_content(content);
return http_response;
}
return nullptr;
}
void LoadIframe(const GURL& url) {
// The script executed here isn't large enough to be eligible for the
// Generated Code Cache and therefore shouldn't affect any metrics by being
// run.
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
JsReplace("const iframe = document.createElement('iframe');"
"iframe.src = $1;"
"document.body.appendChild(iframe);",
url)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
}
protected:
base::OnceClosure done_callback_;
private:
base::test::ScopedFeatureList feature_split_cache_by_network_isolation_key_;
base::test::ScopedFeatureList feature_third_party_storage_partitioning_;
base::test::ScopedFeatureList
feature_split_code_cache_by_network_isolation_key_;
};
INSTANTIATE_TEST_SUITE_P(
All,
CodeCacheBrowserTest,
testing::ValuesIn({CodeCacheTestCase::kCachePartitioningEnabled,
CodeCacheTestCase::kCachePartitioningDisabled}),
[](const testing::TestParamInfo<CodeCacheTestCase>& info) {
switch (info.param) {
case (CodeCacheTestCase::kCachePartitioningEnabled):
return "CachePartitioningEnabled";
case (CodeCacheTestCase::kCachePartitioningDisabled):
return "CachePartitioningDisabled";
}
});
IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest, CachingFromThirdPartyFrames) {
GURL a_com_parent_page =
embedded_test_server()->GetURL("a.com", "/empty.html");
GURL b_com_parent_page =
embedded_test_server()->GetURL("b.com", "/empty.html");
GURL c_com_cacheable_js_page =
embedded_test_server()->GetURL("c.com", "/cacheable.html");
{
// Navigate to the parent page and load an iframe that requests a cacheable
// javascript resource (/cacheable.js). This should result in one
// GeneratedCodeCache miss and one create.
base::HistogramTester histogram_tester;
EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));
LoadIframe(c_com_cacheable_js_page);
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 1);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kCreate, 1);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 0);
}
{
// Navigate to the same test page again and check for a GeneratedCodeCache
// hit.
base::HistogramTester histogram_tester;
EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));
LoadIframe(c_com_cacheable_js_page);
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 1);
}
{
// Navigate to a page with a different top-level site and verify that:
//
// - When cache partitioning is disabled, /cacheable.js is served from the
// GeneratedCodeCache, since resources will be cached per-origin.
//
// - When cache partitioning is enabled, /cacheable.js is not served from
// the GeneratedCodeCache, since resources are also partitioned by the
// top-level site.
base::HistogramTester histogram_tester;
EXPECT_TRUE(NavigateToURL(shell(), b_com_parent_page));
LoadIframe(c_com_cacheable_js_page);
FetchHistogramsFromChildProcesses();
// Note: We don't check the kCreate counts below because for some reason,
// the previous part of the test causes the /cacheable.js entry to be doomed
// and then re-created in this part of the test (so the kCreate count is
// always one more than we'd expect).
if (IsCachePartitioningEnabled()) {
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 1);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 0);
} else {
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 1);
}
}
}
IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest,
CachingFromThirdPartySharedWorkers) {
if (!SupportsSharedWorker()) {
return;
}
GURL a_com_parent_page =
embedded_test_server()->GetURL("a.com", "/empty.html");
GURL b_com_parent_page =
embedded_test_server()->GetURL("b.com", "/empty.html");
GURL c_com_worker_js_page =
embedded_test_server()->GetURL("c.com", "/shared-worker.html");
{
// Navigate to a parent page and create a cross-site iframe that creates a
// SharedWorker which attempts to load a cacheable script via importScripts.
// Create a callback that will get called when the worker requests /done.js,
// indicating that /worker.js and /cacheable.js have been loaded and we can
// proceed with checking histograms.
base::test::TestFuture<void> worker_done;
done_callback_ = worker_done.GetCallback();
base::HistogramTester histogram_tester;
EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));
LoadIframe(c_com_worker_js_page);
ASSERT_TRUE(worker_done.Wait());
FetchHistogramsFromChildProcesses();
// TODO(https://crbug.com/964467): The kMiss and kCreate counts are zero
// here because for Dedicated Workers and Shared Workers the Generated Code
// Cache isn't used yet. Once it is, update these counts (there are two
// scripts that could be cached, depending on the implementation -
// worker.js, the script that contains the Shared Worker code, and
// cacheable.js, which is loaded by the Shared Worker via a call to
// importScripts).
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kCreate, 0);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 0);
}
{
base::test::TestFuture<void> worker_done;
done_callback_ = worker_done.GetCallback();
base::HistogramTester histogram_tester;
EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));
LoadIframe(c_com_worker_js_page);
ASSERT_TRUE(worker_done.Wait());
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
// TODO(https://crbug.com/964467): Once the Generated Code Cache is used for
// Shared Workers, check that the scripts loaded from the worker context
// were re-used from the test section above.
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 0);
}
{
base::test::TestFuture<void> worker_done;
done_callback_ = worker_done.GetCallback();
base::HistogramTester histogram_tester;
EXPECT_TRUE(NavigateToURL(shell(), b_com_parent_page));
LoadIframe(c_com_worker_js_page);
ASSERT_TRUE(worker_done.Wait());
FetchHistogramsFromChildProcesses();
// TODO(https://crbug.com/964467): Once the Generated Code Cache is used
// for Shared Workers, check that the worker scripts re-used in the test
// section above do not get re-used for this part of the test when
// `IsCachePartitioningEnabled()` returns true (but are re-used otherwise).
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
histogram_tester.ExpectBucketCount(
"SiteIsolatedCodeCache.JS.Behaviour",
GeneratedCodeCache::CacheEntryStatus::kHit, 0);
}
}
} // namespace content