blob: ee3de5012ee9b54d5f11cbc762844a7f70ed8204 [file] [log] [blame]
// Copyright 2025 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/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "content/browser/back_forward_cache_browsertest.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
namespace content {
class BackgroundForegroundProcessLimitBackForwardCacheBrowserTest
: public BackForwardCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
EnableCacheSize(kBackForwardCacheSize, kForegroundBackForwardCacheSize);
BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
}
void ExpectCached(const RenderFrameHostImplWrapper& rfh,
bool cached,
bool backgrounded) {
EXPECT_FALSE(rfh.IsDestroyed());
EXPECT_EQ(cached, rfh->IsInBackForwardCache());
EXPECT_EQ(backgrounded, rfh->GetProcess()->GetPriority() ==
base::Process::Priority::kBestEffort);
}
// The number of pages the BackForwardCache can hold per tab.
const size_t kBackForwardCacheSize = 4;
const size_t kForegroundBackForwardCacheSize = 2;
const size_t kPruneSize = 1u;
const NotRestoredReason kPruneReason =
NotRestoredReason::kCacheLimitPrunedOnModerateMemoryPressure;
};
// Test that a series of same-site navigations (which use the same process)
// uses the foreground limit.
IN_PROC_BROWSER_TEST_F(
BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
CacheEvictionSameSite) {
ASSERT_TRUE(embedded_test_server()->Start());
std::vector<RenderFrameHostImplWrapper> rfhs;
for (size_t i = 0; i <= kBackForwardCacheSize * 2; ++i) {
SCOPED_TRACE(i);
GURL url(embedded_test_server()->GetURL(
"a.com", base::StringPrintf("/title1.html?i=%zu", i)));
ASSERT_TRUE(NavigateToURL(shell(), url));
rfhs.emplace_back(current_frame_host());
EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
base::Process::Priority::kBestEffort);
for (size_t j = 0; j <= i; ++j) {
SCOPED_TRACE(j);
// The last page is active, the previous |kForegroundBackForwardCacheSize|
// should be in the cache, any before that should be deleted.
if (i - j <= kForegroundBackForwardCacheSize) {
// All of the processes should be in the foreground.
ExpectCached(rfhs[j], /*cached=*/i != j,
/*backgrounded=*/false);
} else {
ASSERT_TRUE(rfhs[j].WaitUntilRenderFrameDeleted());
}
}
}
// Navigate back but not to the initial about:blank.
for (size_t i = 0; i <= kBackForwardCacheSize * 2 - 1; ++i) {
SCOPED_TRACE(i);
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The first |kBackForwardCacheSize| navigations should be restored from the
// cache. The rest should not.
if (i < kForegroundBackForwardCacheSize) {
ExpectRestored(FROM_HERE);
} else {
ExpectNotRestored({NotRestoredReason::kForegroundCacheLimit}, {}, {}, {},
{}, FROM_HERE);
}
}
}
// Test that a series of cross-site navigations (which use different processes)
// use the background limit.
//
// TODO(crbug.com/40179515): This test is flaky. It has been re-enabled with
// improved failure output (https://crrev.com/c/2862346). It's OK to disable it
// again when it fails.
IN_PROC_BROWSER_TEST_F(
BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
CacheEvictionCrossSite) {
ASSERT_TRUE(embedded_test_server()->Start());
std::vector<RenderFrameHostImplWrapper> rfhs;
for (size_t i = 0; i <= kBackForwardCacheSize * 2; ++i) {
SCOPED_TRACE(i);
// Note: do NOT use .com domains here because a4.com is on the HSTS preload
// list, which will cause our test requests to timeout.
GURL url(embedded_test_server()->GetURL(base::StringPrintf("a%zu.test", i),
"/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url));
rfhs.emplace_back(current_frame_host());
EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
base::Process::Priority::kBestEffort);
for (size_t j = 0; j <= i; ++j) {
SCOPED_TRACE(j);
// The last page is active, the previous |kBackgroundBackForwardCacheSize|
// should be in the cache, any before that should be deleted.
if (i - j <= kBackForwardCacheSize) {
EXPECT_FALSE(rfhs[j].IsDestroyed());
// Pages except the active one should be cached and in the background.
ExpectCached(rfhs[j], /*cached=*/i != j,
/*backgrounded=*/i != j);
} else {
ASSERT_TRUE(rfhs[j].WaitUntilRenderFrameDeleted());
}
}
}
// Navigate back but not to the initial about:blank.
for (size_t i = 0; i <= kBackForwardCacheSize * 2 - 1; ++i) {
SCOPED_TRACE(i);
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The first |kBackForwardCacheSize| navigations should be restored from the
// cache. The rest should not.
if (i < kBackForwardCacheSize) {
ExpectRestored(FROM_HERE);
} else {
ExpectNotRestored({NotRestoredReason::kCacheLimit}, {}, {}, {}, {},
FROM_HERE);
}
}
}
// Test that pruning a series of cross-site navigations (which use different
// processes) evicts the right entries with the right reason.
IN_PROC_BROWSER_TEST_F(
BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
PruneCrossSite) {
ASSERT_TRUE(embedded_test_server()->Start());
std::vector<RenderFrameHostImplWrapper> rfhs;
for (size_t i = 0; i < kBackForwardCacheSize; ++i) {
SCOPED_TRACE(i);
// Note: do NOT use .com domains here because a4.com is on the HSTS preload
// list, which will cause our test requests to timeout.
GURL url(embedded_test_server()->GetURL(base::StringPrintf("a%zu.test", i),
"/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url));
rfhs.emplace_back(current_frame_host());
EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
base::Process::Priority::kBestEffort);
}
CHECK_LE(kPruneSize, kBackForwardCacheSize);
// Prune the BFCache entries.
web_contents()->GetController().GetBackForwardCache().Prune(kPruneSize,
kPruneReason);
for (int i = kBackForwardCacheSize - 1 - 1 - kPruneSize; i >= 0; --i) {
SCOPED_TRACE(i);
ASSERT_TRUE(rfhs[i].WaitUntilRenderFrameDeleted());
}
// Navigate back but not to the initial about:blank.
for (size_t i = 0; i < kBackForwardCacheSize - 1; ++i) {
SCOPED_TRACE(i);
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The first `kPruneSize` navigation should be restored from the cache. The
// rest should not.
if (i < kPruneSize) {
ExpectRestored(FROM_HERE);
} else {
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
}
}
namespace {
const char kPrioritizedPageURL[] = "search.result";
} // namespace
class BackForwardCacheLimitForPrioritizedPagesBrowserTest
: public BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
public testing::WithParamInterface<std::string> {
protected:
// Mock subclass of ContentBrowserClient that will determine if the url is
// prioritized by checking against `kPrioritizedPageURL`.
class MockContentBrowserClientWithPrioritizedBackForwardCacheEntry
: public ContentBrowserTestContentBrowserClient {
public:
// ContentBrowserClient overrides:
bool ShouldPrioritizeForBackForwardCache(BrowserContext* browser_context,
const GURL& url) override {
return url.DomainIs(kPrioritizedPageURL);
}
};
void SetUpOnMainThread() override {
BackgroundForegroundProcessLimitBackForwardCacheBrowserTest::
SetUpOnMainThread();
test_client_ = std::make_unique<
MockContentBrowserClientWithPrioritizedBackForwardCacheEntry>();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
EnableFeatureAndSetParams(kBackForwardCachePrioritizedEntry, "level",
GetParam());
BackgroundForegroundProcessLimitBackForwardCacheBrowserTest::
SetUpCommandLine(command_line);
}
bool ShouldPrioritizeWhenClearAllUnlessNoEviction() {
return GetParam() == "prioritize-unless-should-clear-all-and-no-eviction";
}
private:
std::unique_ptr<MockContentBrowserClientWithPrioritizedBackForwardCacheEntry>
test_client_;
};
INSTANTIATE_TEST_SUITE_P(
All,
BackForwardCacheLimitForPrioritizedPagesBrowserTest,
testing::Values("prioritize-unless-should-clear-all",
"prioritize-unless-should-clear-all-and-no-eviction"));
// Test that both when pruning with size 0, if no other eviction happens, the
// prioritized entry would be evicted.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
PruneToZero_NoOtherEvictionHappens) {
ASSERT_TRUE(embedded_test_server()->Start());
// We need at least 1 entries in the BFCache list for this test.
CHECK_GE(kBackForwardCacheSize, 1u);
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
// Now the BFCache entry list is: [pp, b].
// Prune the BFCache entries to 0.
web_contents()->GetController().GetBackForwardCache().Prune(0, kPruneReason);
// All the entries should be evicted
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
// Test that both when pruning with size 0, if there is another entry evicted,
// the prioritized entry would be:
// - evicted if the level is prioritize-unless-should-clear-all
// - not evicted if the level is
// prioritize-unless-should-clear-all-and-no-eviction.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
PruneToZero_OtherEvictionHappens) {
ASSERT_TRUE(embedded_test_server()->Start());
// We need at least 2 entries in the BFCache list for this test.
CHECK_GE(kBackForwardCacheSize, 2u);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
// Now the BFCache entry list is: [a, pp, b].
// Prune the BFCache entries to 0.
web_contents()->GetController().GetBackForwardCache().Prune(0, kPruneReason);
ASSERT_TRUE(HistoryGoBack(web_contents()));
if (ShouldPrioritizeWhenClearAllUnlessNoEviction()) {
// If the level is prioritize-unless-should-clear-all-and-no-eviction, the
// prioritized entry should be restored since the some other eviction
// happens.
ExpectRestored(FROM_HERE);
} else {
// Otherwise the prioritized entry should be evicted as well.
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
// The non-prioritized entry should always be evicted.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
// Test that when pruning with a positive number size, the last prioritized
// entry outside the limit will not be evicted.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
PruneToNonZero_PrioritizedEntryOutsideLimit) {
ASSERT_TRUE(embedded_test_server()->Start());
// We need at least 4 entries in the BFCache list for this test.
CHECK_GE(kBackForwardCacheSize, 4u);
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title2.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("c.test", "/title1.html")));
// Now the BFCache entry list is: [pe1, a, pe2, b].
// Prune the BFCache entries to 1, the result should be:
// [pe1(evicted), a(evicted), pe2(prioritized entry special rule), b].
web_contents()->GetController().GetBackForwardCache().Prune(1, kPruneReason);
// The last non-prioritized entry should be restored because it's within the
// cache limit.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// The last prioritized entry should be restored since it's the special
// prioritized entry.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// The other entries (including the prioritized one) should not be restored.
for (size_t i = 0; i < 2; ++i) {
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
}
// Test that when pruning with a positive number size, the last prioritized
// entry inside the limit should be counted as the regular cache.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
PruneToNonZero_PrioritizedEntryInsideLimit) {
ASSERT_TRUE(embedded_test_server()->Start());
// We need at least 2 entries in the BFCache list for this test.
CHECK_GE(kBackForwardCacheSize, 2u);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title2.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
// Now the BFCache entry list is: [a, pe].
// Prune the BFCache entries to 1, the result should be:
// [a(evicted), pe].
web_contents()->GetController().GetBackForwardCache().Prune(1, kPruneReason);
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
// Test that when pruning with a positive number size while there is already an
// old prioritized entry kept in cache before, it will be replaced by the newer
// prioritized entry.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
PruneToNonZeroTwice) {
ASSERT_TRUE(embedded_test_server()->Start());
// We need at least 4 entries in the BFCache list for this test.
CHECK_LE(kBackForwardCacheSize, 4u);
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
// Now the BFCache entry list is: [pe1, a].
// Prune the BFCache entries to 1, the result should still be
// [pe1(prioritized entry special rule), a].
web_contents()->GetController().GetBackForwardCache().Prune(1, kPruneReason);
// The last non-prioritized entry should be restored because it's within the
// cache limit.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// The last prioritized entry should be restored since it's the special
// prioritized entry.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("c.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title2.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("d.test", "/title1.html")));
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("e.test", "/title1.html")));
// Now the BFCache entry list is: [pe1, c, pe2, d].
// Prune the BFCache entries to 1, the result should still be
// [pe1(evicted), a(evicted), pe2(prioritized entry special rule), d].
web_contents()->GetController().GetBackForwardCache().Prune(1, kPruneReason);
// The last non-prioritized entry should be restored because it's within the
// cache limit.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// The last prioritized entry should be restored since it's the special
// prioritized entry.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
// The other entries (including the prioritized one) should not be restored.
for (size_t i = 0; i < 2; ++i) {
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}
}
// Test that the prioritized BFCache entry will not be evicted even when another
// entry is stored and exceeds the limit.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
CacheLimitReached) {
ASSERT_TRUE(embedded_test_server()->Start());
// We need at least 1 entry in the BFCache list for this test.
CHECK_GE(kBackForwardCacheSize, 1u);
ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
kPrioritizedPageURL, "/title1.html")));
// Fill the BFCache with more entry and make it just exceeds the limit, the
// result should be:
// [pe(prioritized entry special rule), a0, a1, ...].
for (size_t i = 0; i <= kBackForwardCacheSize; ++i) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
base::StringPrintf("a%zu.test", i), "/title1.html")));
}
// For the entries within cache size limit, they should be restored.
for (size_t i = 0; i < kBackForwardCacheSize; ++i) {
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// The prioritized entry should be restored as well even if it's outside the
// limit.
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
}
// Test that the cache responds to processes switching from background to
// foreground. We set things up so that we have
// Cached sites:
// a0.test
// a1.test
// a2.test
// a3.test
// and the active page is a4.test. Then set the process for a[1-3] to
// foregrounded so that there are 3 entries whose processes are foregrounded.
// BFCache should evict the eldest (a1) leaving a0 because despite being older,
// it is backgrounded. Setting the priority directly is not ideal but there is
// no reliable way to cause the processes to go into the foreground just by
// navigating because proactive browsing instance swap makes it impossible to
// reliably create a new a1.test renderer in the same process as the old
// a1.test.
//
// Note that we do NOT use .com domains because a4.com is on the HSTS preload
// list. Since our test server doesn't use HTTPS, using a4.com results in the
// test timing out.
IN_PROC_BROWSER_TEST_F(
BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
ChangeToForeground) {
ASSERT_TRUE(embedded_test_server()->Start());
std::vector<RenderFrameHostImplWrapper> rfhs;
// Navigate through a[0-3].com.
for (size_t i = 0; i < kBackForwardCacheSize; ++i) {
SCOPED_TRACE(i);
GURL url(embedded_test_server()->GetURL(base::StringPrintf("a%zu.test", i),
"/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url));
rfhs.emplace_back(current_frame_host());
EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
base::Process::Priority::kBestEffort);
}
// Check that a0-2 are cached and backgrounded.
for (size_t i = 0; i < kBackForwardCacheSize - 1; ++i) {
SCOPED_TRACE(i);
ExpectCached(rfhs[i], /*cached=*/true, /*backgrounded=*/true);
}
// Navigate to a page which causes the processes for a[1-3] to be
// foregrounded.
GURL url(embedded_test_server()->GetURL("a4.test", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell(), url));
// Assert that we really have set up the situation we want where the processes
// are shared and in the foreground.
RenderFrameHostImpl* rfh = current_frame_host();
ASSERT_NE(rfh->GetProcess()->GetPriority(),
base::Process::Priority::kBestEffort);
rfhs[1]->GetProcess()->OnMediaStreamAdded();
rfhs[2]->GetProcess()->OnMediaStreamAdded();
rfhs[3]->GetProcess()->OnMediaStreamAdded();
// The page should be evicted.
ASSERT_TRUE(rfhs[1].WaitUntilRenderFrameDeleted());
// Check that a0 is cached and backgrounded.
ExpectCached(rfhs[0], /*cached=*/true, /*backgrounded=*/true);
// Check that a2-3 are cached and foregrounded.
ExpectCached(rfhs[2], /*cached=*/true, /*backgrounded=*/false);
ExpectCached(rfhs[3], /*cached=*/true, /*backgrounded=*/false);
}
} // namespace content