blob: 18753ab762fca42c729503ada3b7b986debd2fc7 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <string_view>
#include "base/feature_list.h"
#include "base/notimplemented.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/to_string.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/renderer.mojom.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/shell/browser/shell.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#if !BUILDFLAG(IS_FUCHSIA)
namespace {
// See the definition of `v8::internal::ScriptCompileTimerScope::CacheBehaviour`
// in `v8/src/codegen/compiler.cc` for the correspondence.
int CacheBehaviourNameToInt(std::string_view name) {
if (name == "kProduceCodeCache") {
return 0;
}
if (name == "kHitIsolateCacheWhenNoCache") {
return 1;
}
if (name == "kConsumeCodeCache") {
return 2;
}
if (name == "kNoCacheBecauseScriptTooSmall") {
return 5;
}
if (name == "kNoCacheBecauseCacheTooCold") {
return 6;
}
if (name == "kHitIsolateCacheWhenProduceCodeCache") {
return 14;
}
if (name == "kHitIsolateCacheWhenConsumeCodeCache") {
return 15;
}
if (name == "kNoCacheBecauseInlineScriptCacheTooCold") {
return 22;
}
// TODO: Add other values if necessary.
NOTIMPLEMENTED();
return -1;
}
int GetProduceCacheCount(const base::HistogramTester& histogram_tester) {
return histogram_tester.GetBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kProduceCodeCache")) +
histogram_tester.GetBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kHitIsolateCacheWhenProduceCodeCache"));
}
int GetConsumeCacheCount(const base::HistogramTester& histogram_tester) {
return histogram_tester.GetBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kConsumeCodeCache")) +
histogram_tester.GetBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kHitIsolateCacheWhenConsumeCodeCache"));
}
} // namespace
namespace content {
class InlineScriptCodeCacheBrowserTest : public ContentBrowserTest {
public:
InlineScriptCodeCacheBrowserTest() {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{net::features::kSplitCacheByNetworkIsolationKey, {}},
{net::features::kSplitCodeCacheByNetworkIsolationKey, {}},
{blink::features::kUsePersistentCacheForCodeCache, {}},
{blink::features::kInlineScriptCache, {{"timeout", "1s"}}}},
{{}});
}
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&InlineScriptCodeCacheBrowserTest::RequestHandler,
base::Unretained(this)));
ASSERT_TRUE(embedded_test_server()->Start());
}
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
const net::test_server::HttpRequest& request) {
GURL absolute_url = embedded_test_server()->GetURL(request.relative_url);
if (absolute_url.GetPath() == "/inline-script.html" ||
absolute_url.GetPath() == "/same-inline-script.html") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
std::string title = absolute_url.GetPath() == "/inline-script.html"
? "A Page"
: "A PageDifferent Page";
// Run a short, heavy script before the test script to warmup connection
// to the inline script cache backend.
std::string content =
base::StrCat({"<html><head><title>", title, "</title></head>",
"<body><script type='text/javascript'>",
GenerateUniqueHeavyJavascriptSource(),
"</script><script type='text/javascript'>",
GetCacheableLongStaticJavascriptSource(),
"</script></body></html>"});
http_response->set_content(content);
http_response->set_content_type("text/html");
return http_response;
}
if (absolute_url.GetPath() == "/short-inline-script.html") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
// HTML with a very short inline script.
std::string short_script = "/*short*/";
EXPECT_LT(short_script.length(),
blink::features::kInlineScriptCacheMinScriptLength.Get());
std::string content =
base::StrCat({"<html><head><title>Title</title></head><body>",
"<script type='text/javascript'>", short_script,
"</script></body></html>\n"});
http_response->set_content(content);
http_response->set_content_type("text/html");
return http_response;
}
if (absolute_url.GetPath() == "/empty.html") {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content("<html><body></body></html>");
http_response->set_content_type("text/html");
return http_response;
}
return nullptr;
}
void PurgeResourceCacheFromTheMainFrame() {
base::RunLoop loop;
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->current_frame_host()
->GetProcess()
->GetRendererInterface()
->PurgeResourceCache(loop.QuitClosure());
loop.Run();
}
void LoadIframe(const GURL& url) {
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()));
}
void PurgeResourceCacheFromTheFirstSubFrame() {
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
CHECK(root->child_count() != 0);
base::RunLoop loop;
root->child_at(root->child_count() - 1)
->current_frame_host()
->GetProcess()
->GetRendererInterface()
->PurgeResourceCache(loop.QuitClosure());
loop.Run();
}
private:
std::string GenerateUniqueHeavyJavascriptSource() {
++served_count_;
// A time-consuming script to run.
std::string source =
base::StrCat({"const a=[...Array(10000000).keys()];let s=",
base::NumberToString(served_count_), ";",
"for(let i=0;i<a.length;i++)s+=i*(i+1)-a[i];"
"console.log(s);"});
EXPECT_LT(source.length(),
blink::features::kInlineScriptCacheMinScriptLength.Get());
return source;
}
// A script longer enough to be cached.
std::string GetCacheableLongStaticJavascriptSource() {
return base::StrCat(
{"function testFunction() {\n",
std::string(blink::features::kInlineScriptCacheMinScriptLength.Get(),
'/'),
"\nreturn 42;\n}"});
}
base::test::ScopedFeatureList scoped_feature_list_;
uint32_t served_count_ = 0;
};
// TODO(crbug.com/498265776): Test is failing on ChromeOS and Linux MSan.
#if (BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)) && defined(MEMORY_SANITIZER)
#define MAYBE_CacheProducedOnSecondAttempt DISABLED_CacheProducedOnSecondAttempt
#else
#define MAYBE_CacheProducedOnSecondAttempt CacheProducedOnSecondAttempt
#endif
IN_PROC_BROWSER_TEST_F(InlineScriptCodeCacheBrowserTest,
MAYBE_CacheProducedOnSecondAttempt) {
GURL url =
embedded_test_server()->GetURL("example.com", "/inline-script.html");
// Step 1: Load the page first time. No cache is used.
{
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url));
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kNoCacheBecauseInlineScriptCacheTooCold"), 1);
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 2: Reload the page. The script will be compiled from source and then
// producing the cache to store it on persistent storage.
{
bool produced = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url));
FetchHistogramsFromChildProcesses();
if (GetProduceCacheCount(histogram_tester) == 1) {
produced = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(produced) << "Failed to produce cache";
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 3: Reload the page again. The script will be compiled from code cache.
{
bool consumed = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url));
FetchHistogramsFromChildProcesses();
if (GetConsumeCacheCount(histogram_tester) == 1) {
consumed = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(consumed) << "Failed to consume cache";
}
}
// TODO(crbug.com/498265776): Test is failing on ChromeOS and Linux MSan.
#if (BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)) && defined(MEMORY_SANITIZER)
#define MAYBE_CacheSharedOnDifferentPage DISABLED_CacheSharedOnDifferentPage
#else
#define MAYBE_CacheSharedOnDifferentPage CacheSharedOnDifferentPage
#endif
IN_PROC_BROWSER_TEST_F(InlineScriptCodeCacheBrowserTest,
MAYBE_CacheSharedOnDifferentPage) {
GURL url_1 =
embedded_test_server()->GetURL("example.com", "/inline-script.html");
GURL url_2 =
embedded_test_server()->GetURL("example.com", "/same-inline-script.html");
// Step 1: Load the page first time. No cache is used.
{
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url_1));
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kNoCacheBecauseInlineScriptCacheTooCold"), 1);
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 2: Reload the page. The script will be compiled from source and then
// producing the cache to store it on persistent storage.
{
bool produced = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url_1));
FetchHistogramsFromChildProcesses();
if (GetProduceCacheCount(histogram_tester) == 1) {
produced = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(produced) << "Failed to produce cache";
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 3: Load the different URL with the same NIK and the same inline
// script. The script will be compiled from code cache.
{
bool consumed = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url_2));
FetchHistogramsFromChildProcesses();
if (GetConsumeCacheCount(histogram_tester) == 1) {
consumed = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(consumed) << "Failed to consume cache";
}
}
IN_PROC_BROWSER_TEST_F(InlineScriptCodeCacheBrowserTest,
NotProducedForShortScript) {
// Even after multiple page loads, the code cache should not be produced for
// short scripts.
const int num_tries = 3;
base::HistogramTester histogram_tester;
for (int i = 0; i < num_tries; i++) {
// Use different query parameters so that BFCache would not work.
GURL url = embedded_test_server()->GetURL(
"example.com", base::StrCat({"/short-inline-script.html?i=",
base::NumberToString(i)}));
ASSERT_TRUE(NavigateToURL(shell(), url));
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kNoCacheBecauseScriptTooSmall"), num_tries);
}
// TODO(crbug.com/498265776): Test is failing on ChromeOS and Linux MSan.
#if (BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)) && defined(MEMORY_SANITIZER)
#define MAYBE_IsolatedByNik DISABLED_IsolatedByNik
#else
#define MAYBE_IsolatedByNik IsolatedByNik
#endif
IN_PROC_BROWSER_TEST_F(InlineScriptCodeCacheBrowserTest, MAYBE_IsolatedByNik) {
GURL top_domain_a =
embedded_test_server()->GetURL("a.example", "/empty.html");
GURL iframe_domain_b =
embedded_test_server()->GetURL("b.example", "/inline-script.html");
GURL top_domain_c =
embedded_test_server()->GetURL("c.example", "/empty.html");
// Step 1: Load top_domain_a and embed iframe_domain_b. Cache is cold.
{
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), top_domain_a));
LoadIframe(iframe_domain_b);
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kNoCacheBecauseInlineScriptCacheTooCold"), 1);
}
PurgeResourceCacheFromTheMainFrame();
PurgeResourceCacheFromTheFirstSubFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 2: Reload the same setup (top A, iframe B). Cache should be produced
// or it could hit the Isolate cache if the process is reused.
{
bool produced = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), top_domain_a));
LoadIframe(iframe_domain_b);
FetchHistogramsFromChildProcesses();
if (GetProduceCacheCount(histogram_tester) == 1) {
produced = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
PurgeResourceCacheFromTheFirstSubFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(produced) << "Failed to produce cache";
}
PurgeResourceCacheFromTheMainFrame();
PurgeResourceCacheFromTheFirstSubFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 3: Load top_domain_c and embed iframe_domain_b.
// Since NIK splits cache, there should be no inline script cache created by
// domain A while it could hit the Isolate cache if the process is reused.
{
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), top_domain_c));
LoadIframe(iframe_domain_b);
FetchHistogramsFromChildProcesses();
int no_cache_count =
histogram_tester.GetBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt(
"kNoCacheBecauseInlineScriptCacheTooCold")) +
histogram_tester.GetBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kHitIsolateCacheWhenNoCache"));
EXPECT_EQ(no_cache_count, 1);
}
}
// TODO(crbug.com/498265776): Test is failing on ChromeOS and Linux MSan.
#if (BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)) && defined(MEMORY_SANITIZER)
#define MAYBE_ProducedCacheHitsOnAnotherProcess \
DISABLED_ProducedCacheHitsOnAnotherProcess
#else
#define MAYBE_ProducedCacheHitsOnAnotherProcess \
ProducedCacheHitsOnAnotherProcess
#endif
IN_PROC_BROWSER_TEST_F(InlineScriptCodeCacheBrowserTest,
MAYBE_ProducedCacheHitsOnAnotherProcess) {
GURL url = embedded_test_server()->GetURL("a.example", "/inline-script.html");
// Step 1: Load the page first time.
{
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url));
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectBucketCount(
"V8.CompileScript.CacheBehaviour",
CacheBehaviourNameToInt("kNoCacheBecauseInlineScriptCacheTooCold"), 1);
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
// Step 2: Reload the page to produce the cache.
{
bool produced = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url));
FetchHistogramsFromChildProcesses();
if (GetProduceCacheCount(histogram_tester) == 1) {
produced = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(produced) << "Failed to produce cache";
}
// Step 3: Recreate the window to ensure the next load happens in a completely
// new renderer process.
RecreateWindow();
// Step 4: Load the page again. Since it's a new process, it cannot hit the
// in-memory Isolate cache. It must strictly hit the persistent code cache.
{
bool consumed = false;
for (int i = 0; i < 5; i++) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url));
FetchHistogramsFromChildProcesses();
if (GetConsumeCacheCount(histogram_tester) == 1) {
consumed = true;
break;
}
PurgeResourceCacheFromTheMainFrame();
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
}
EXPECT_TRUE(consumed) << "Failed to consume cache";
}
}
} // namespace content
#endif // !BUILDFLAG(IS_FUCHSIA)