0

Reland "Hook ClosedTabCache into the tab restore flow"

Fix failing ClosedTabCache browser tests on linux-bfcache-rel.

Due to an uninitialized member in variable in ClosedTabCache,
it's browser tests were not running as expected on linux-bfcache-rel
when calling ClosedTabCache::CanCacheWebContents(). This reland fixes
failing tests by properly initializing |memory_pressure_level_| and
adding the browser test MemoryPressureLevelModerateThenCritical.

This is a reland of 6ee42a1cd1

Original change's description:
> Hook ClosedTabCache into the tab restore flow
>
> If the feature kClosedTabCache is enabled, we store the WebContents in
> the ClosedTabCache instead of destroying it. This is achieved by
> hooking the caching logic in TabStripModel.
>
> TabStripModel::DetachedWebContents struct has been modified to also
> store the associated SessionID and made public.
>
> TabStripModel::DetachWebContentsImpl has been modified to return a
> DetachedWebContents, instead of a std::unique_ptr<WebContents>.
>
> TabStripModel::CloseWebContentses has been modified to let the delegate
> cache the |owned_contents| of DetachedWebContents, if possible.
>
> BrowserTabStripModelDelegate::CacheWebContents has been introduced to
> determine if DetachedWebContents |owned_contents| are cacheable, and if
> so to transfer the ownership to ClosedTabCache.
>
> Design doc:
> https://docs.google.com/document/d/1SF230MYWgroe4WikDMn82ETd3-HFRzylKtIk0HZRaOU/edit?usp=sharing
>
> BUG=1100946
>
> Change-Id: I8452a72b52aa21b04f53656c6571cd5e5fb65257
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2987788
> Commit-Queue: Tobias Soppa <tobias.soppa@code.berlin>
> Reviewed-by: Scott Violet <sky@chromium.org>
> Reviewed-by: Sreeja Kamishetty <sreejakshetty@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#908058}

Bug: 1100946
Change-Id: I3f162ee703e5b79bda84d111ab07b0a07cd7d01b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3069288
Reviewed-by: Scott Violet <sky@chromium.org>
Reviewed-by: Sreeja Kamishetty <sreejakshetty@chromium.org>
Commit-Queue: Tobias Soppa <tobias.soppa@code.berlin>
Cr-Commit-Position: refs/heads/master@{#911279}
This commit is contained in:
Tobias Soppa
2021-08-12 13:02:01 +00:00
committed by Chromium LUCI CQ
parent d7e577453b
commit 08280881f0
16 changed files with 592 additions and 283 deletions

@ -1103,6 +1103,7 @@ Timo Reimann <ttr314@googlemail.com>
Timo Witte <timo.witte@gmail.com>
Ting Shao <ting.shao@intel.com>
Tobias Soppa <tobias@soppa.me>
Tobias Soppa <tobias.soppa@code.berlin>
Tom Callaway <tcallawa@redhat.com>
Tom Harwood <tfh@skip.org>
Tomas Popela <tomas.popela@gmail.com>

@ -1502,8 +1502,6 @@ static_library("browser") {
"sessions/chrome_serialized_navigation_driver.h",
"sessions/chrome_tab_restore_service_client.cc",
"sessions/chrome_tab_restore_service_client.h",
"sessions/closed_tab_cache.cc",
"sessions/closed_tab_cache.h",
"sessions/restore_on_startup_policy_handler.cc",
"sessions/restore_on_startup_policy_handler.h",
"sessions/session_common_utils.cc",
@ -4067,6 +4065,8 @@ static_library("browser") {
"serial/serial_chooser_histograms.h",
"serial/serial_policy_allowed_ports.cc",
"serial/serial_policy_allowed_ports.h",
"sessions/closed_tab_cache.cc",
"sessions/closed_tab_cache.h",
"sessions/closed_tab_cache_service.cc",
"sessions/closed_tab_cache_service.h",
"sessions/closed_tab_cache_service_factory.cc",

@ -9,7 +9,6 @@
#include "base/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/browser_features.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
@ -48,25 +47,43 @@ ClosedTabCache::ClosedTabCache()
}
ClosedTabCache::~ClosedTabCache() = default;
base::TimeDelta ClosedTabCache::GetTimeToLiveInClosedTabCache() {
// We use the following order of priority if multiple values exist:
// - The programmatical value set in params. Used in specific tests.
// - Default value otherwise, kDefaultTimeToLiveInClosedTabCacheInSeconds.
bool ClosedTabCache::CanCacheWebContents(absl::optional<SessionID> id) {
TRACE_EVENT0("browser", "ClosedTabCache::CanCacheWebContents");
return base::TimeDelta::FromSeconds(base::GetFieldTrialParamByFeatureAsInt(
features::kClosedTabCache, "time_to_live_in_closed_tab_cache_in_seconds",
kDefaultTimeToLiveInClosedTabCacheInSeconds.InSeconds()));
// Only store if the kClosedTabCache feature is enabled.
if (!base::FeatureList::IsEnabled(features::kClosedTabCache))
return false;
// We need to assume that the caller took care of obtaining a SessionID.
DCHECK(id.has_value());
// Only store if tab has valid session id associated with it.
if (!id.value().is_valid())
return false;
// If the current memory pressure exceeds the threshold, we should not cache
// any WebContents. `memory_pressure_level_` is initialized to
// MEMORY_PRESSURE_LEVEL_NONE and will only be updated if the feature gets
// enabled, thus this branch won't be taken if the feature is disabled.
if (memory_pressure_level_ >= kClosedTabCacheMemoryPressureThreshold)
return false;
// For all other cases, you can store the tab in ClosedTabCache.
return true;
}
void ClosedTabCache::StoreEntry(SessionID id,
std::unique_ptr<content::WebContents> wc,
base::TimeTicks timestamp) {
TRACE_EVENT2("browser", "ClosedTabCache::StoreEntry", "SessionID", id.id(),
"URL", wc->GetURL().spec());
void ClosedTabCache::CacheWebContents(
std::pair<absl::optional<SessionID>, std::unique_ptr<content::WebContents>>
cached) {
TRACE_EVENT0("browser", "ClosedTabCache::CacheWebContents");
auto entry = std::make_unique<Entry>(id, std::move(wc), timestamp);
// TODO: Dispatch pagehide() before freezing.
DCHECK(CanCacheWebContents(cached.first));
auto entry = std::make_unique<Entry>(
cached.first.value(), std::move(cached.second), base::TimeTicks::Now());
// TODO(https://crbug.com/1117377): Add a WebContents::SetInClosedTabCache()
// method to replace freezing the page.
entry->web_contents->WasHidden();
DCHECK_EQ(content::Visibility::HIDDEN, entry->web_contents->GetVisibility());
entry->web_contents->SetPageFrozen(/*frozen=*/true);
StartEvictionTimer(entry.get());
@ -107,6 +124,46 @@ const content::WebContents* ClosedTabCache::GetWebContents(SessionID id) const {
return (*matching_entry).get()->web_contents.get();
}
base::TimeDelta ClosedTabCache::GetTimeToLiveInClosedTabCache() {
// We use the following order of priority if multiple values exist:
// - The programmatical value set in params. Used in specific tests.
// - Infinite if kClosedTabCacheNoTimeEviction is enabled.
// - Default value otherwise, kDefaultTimeToLiveInClosedTabCacheInSeconds.
if (base::FeatureList::IsEnabled(kClosedTabCacheNoTimeEviction) &&
GetFieldTrialParamValueByFeature(
features::kClosedTabCache,
"time_to_live_in_closed_tab_cache_in_seconds")
.empty()) {
return base::TimeDelta::Max();
}
return base::TimeDelta::FromSeconds(base::GetFieldTrialParamByFeatureAsInt(
features::kClosedTabCache, "time_to_live_in_closed_tab_cache_in_seconds",
kDefaultTimeToLiveInClosedTabCacheInSeconds.InSeconds()));
}
void ClosedTabCache::SetCacheSizeLimitForTesting(size_t limit) {
cache_size_limit_ = limit;
}
void ClosedTabCache::SetTaskRunnerForTesting(
scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
task_runner_ = task_runner;
}
bool ClosedTabCache::IsEmpty() {
return entries_.empty();
}
// static
bool ClosedTabCache::IsFeatureEnabled() {
return base::FeatureList::IsEnabled(features::kClosedTabCache);
}
size_t ClosedTabCache::EntriesCount() {
return entries_.size();
}
void ClosedTabCache::StartEvictionTimer(Entry* entry) {
base::TimeDelta evict_after = GetTimeToLiveInClosedTabCache();
entry->eviction_timer.SetTaskRunner(task_runner_);
@ -128,26 +185,17 @@ void ClosedTabCache::EvictEntryById(SessionID id) {
entries_.erase(matching_entry);
}
void ClosedTabCache::SetCacheSizeLimitForTesting(size_t limit) {
cache_size_limit_ = limit;
}
void ClosedTabCache::SetTaskRunnerForTesting(
scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
task_runner_ = task_runner;
}
bool ClosedTabCache::IsEmpty() {
return entries_.empty();
}
size_t ClosedTabCache::EntriesCount() {
return entries_.size();
}
void ClosedTabCache::OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel level) {
if (level >= kClosedTabCacheMemoryPressureThreshold)
if (!base::FeatureList::IsEnabled(kClosedTabCacheMemoryPressure)) {
// Don't flush entries if MemoryPressure is disabled for ClosedTabCache.
return;
}
if (memory_pressure_level_ != level)
memory_pressure_level_ = level;
if (memory_pressure_level_ >= kClosedTabCacheMemoryPressureThreshold)
Flush();
}

@ -12,12 +12,23 @@
#include "base/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/browser_features.h"
#include "components/sessions/core/session_id.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace content {
class WebContents;
} // namespace content
// Removes the time limit for cached content. This is used by tests to identify
// accidentally passing tests.
const base::Feature kClosedTabCacheNoTimeEviction{
"ClosedTabCacheNoTimeEviction", base::FEATURE_DISABLED_BY_DEFAULT};
// Enables MemoryPressure for closed tab cache.
const base::Feature kClosedTabCacheMemoryPressure{
"ClosedTabCacheMemoryPressure", base::FEATURE_DISABLED_BY_DEFAULT};
// ClosedTabCache:
//
// A browser feature implemented with the purpose of instantaneously restoring
@ -28,8 +39,6 @@ class WebContents;
// - stores WebContents instances uniquely identified by a SessionID.
// - evicts cache entries after a timeout.
// - evicts the least recently closed tab when the cache is full.
//
// TODO(aebacanu): Hook ClosedTabCache into the tab restore flow.
class ClosedTabCache {
public:
ClosedTabCache();
@ -37,11 +46,15 @@ class ClosedTabCache {
ClosedTabCache& operator=(const ClosedTabCache&) = delete;
~ClosedTabCache();
// Creates a ClosedTabCache::Entry from the given |id|, |wc| and |timestamp|.
// Moves the entry into the ClosedTabCache and evicts one if necessary.
void StoreEntry(SessionID id,
std::unique_ptr<content::WebContents> wc,
base::TimeTicks timestamp);
// ClosedTabCache needs to decide if it could cache a WebContents or not.
bool CanCacheWebContents(absl::optional<SessionID> id);
// Stores all |cacheable_web_contents| in ClosedTabCache. It is assumed that
// each passed WebContents is cacheable. This needs to be checked upfront by
// calling ClosedTabCache::CanCacheWebContents.
void CacheWebContents(
std::pair<absl::optional<SessionID>,
std::unique_ptr<content::WebContents>> cached);
// Moves a WebContents out of ClosedTabCache knowing its |id|. Returns nullptr
// if none is found.
@ -65,6 +78,9 @@ class ClosedTabCache {
// Whether the entries list is empty or not.
bool IsEmpty();
// Returns true if ClosedTabCache feature is currently enabled.
static bool IsFeatureEnabled();
// Get the number of currently stored entries.
size_t EntriesCount();
@ -117,6 +133,11 @@ class ClosedTabCache {
// Listener that sets up a callback to flush the cache if there is not enough
// memory available.
std::unique_ptr<base::MemoryPressureListener> listener_;
// Current `memory_pressure_level_` used to determine if we are able to cache
// an entry or not. Needs to be updated from `listener_` callback.
base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level_ =
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
};
#endif // CHROME_BROWSER_SESSIONS_CLOSED_TAB_CACHE_H_

@ -10,180 +10,189 @@
#include "base/time/time.h"
#include "base/util/memory_pressure/fake_memory_pressure_monitor.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/sessions/closed_tab_cache_service_factory.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/sessions/tab_restore_service_load_waiter.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_tab_strip_model_delegate.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/sessions/core/session_id.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
using content::WebContents;
class ClosedTabCacheTest : public InProcessBrowserTest {
class ClosedTabCacheBrowserTest : public InProcessBrowserTest {
public:
ClosedTabCacheTest() = default;
ClosedTabCacheTest(const ClosedTabCacheTest&) = delete;
ClosedTabCacheTest& operator=(const ClosedTabCacheTest&) = delete;
ClosedTabCacheBrowserTest() = default;
~ClosedTabCacheBrowserTest() override = default;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
}
protected:
// Add a tab to the given browser.
void AddTab(Browser* browser) {
void SetUpCommandLine(base::CommandLine* command_line) override {
feature_list_.InitWithFeaturesAndParameters(
{{features::kClosedTabCache, {}}, {kClosedTabCacheNoTimeEviction, {}}},
{});
InProcessBrowserTest::SetUpCommandLine(command_line);
}
// Add a tab to the given browser and navigate to url.
void NavigateToURL(Browser* browser, const std::string& origin) {
GURL server_url = embedded_test_server()->GetURL(origin, "/title1.html");
ui_test_utils::NavigateToURLWithDisposition(
browser, GURL(chrome::kChromeUINewTabURL),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
browser, server_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
}
void CloseTabAt(int index) {
browser()->tab_strip_model()->CloseWebContentsAt(
index, TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
}
void RestoreMostRecentTab() {
TabRestoreServiceLoadWaiter waiter(
TabRestoreServiceFactory::GetForProfile(browser()->profile()));
chrome::RestoreTab(browser());
waiter.Wait();
}
ClosedTabCache& closed_tab_cache() {
return ClosedTabCacheServiceFactory::GetForProfile(browser()->profile())
->closed_tab_cache();
}
util::test::FakeMemoryPressureMonitor fake_memory_pressure_monitor_;
private:
base::test::ScopedFeatureList feature_list_;
};
// Add an entry to the cache when the cache is empty.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, StoreEntryWhenEmpty) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, StoreEntryWhenEmpty) {
ASSERT_TRUE(embedded_test_server()->Start());
AddTab(browser());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
std::unique_ptr<WebContents> wc =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(SessionID::NewUnique(), std::move(wc),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
CloseTabAt(1);
ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
}
// Add an entry to the cache when there is enough space.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, StoreEntryBasic) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, StoreEntryBasic) {
closed_tab_cache().SetCacheSizeLimitForTesting(2);
cache.SetCacheSizeLimitForTesting(2);
ASSERT_TRUE(embedded_test_server()->Start());
AddTab(browser());
AddTab(browser());
NavigateToURL(browser(), "a.com");
NavigateToURL(browser(), "b.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 3);
std::unique_ptr<WebContents> wc1 =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
std::unique_ptr<WebContents> wc2 =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(SessionID::NewUnique(), std::move(wc1),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
cache.StoreEntry(SessionID::NewUnique(), std::move(wc2),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 2U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 2U);
}
// Add an entry to the cache when the cache is at its limit.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, StoreEntryWhenFull) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, StoreEntryWhenFull) {
ASSERT_TRUE(embedded_test_server()->Start());
AddTab(browser());
AddTab(browser());
NavigateToURL(browser(), "a.com");
NavigateToURL(browser(), "b.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 3);
std::unique_ptr<WebContents> wc1 =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
std::unique_ptr<WebContents> wc2 =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
SessionID id1 = SessionID::NewUnique();
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(id1, std::move(wc1), base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
cache.StoreEntry(SessionID::NewUnique(), std::move(wc2),
base::TimeTicks::Now());
CloseTabAt(1);
// Expect the cache size to still be 1 and the removed entry to be entry1.
EXPECT_EQ(cache.EntriesCount(), 1U);
EXPECT_EQ(cache.GetWebContents(id1), nullptr);
// Expect the cache size to still be 1.
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
}
// Restore an entry when the cache is empty.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, RestoreEntryWhenEmpty) {
ClosedTabCache cache;
ASSERT_TRUE(cache.IsEmpty())
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, RestoreEntryWhenEmpty) {
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
SessionID id = SessionID::NewUnique();
EXPECT_EQ(cache.RestoreEntry(id), nullptr);
EXPECT_EQ(closed_tab_cache().RestoreEntry(id), nullptr);
}
// Restore an entry that is not in the cache.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, RestoreEntryWhenNotFound) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, RestoreEntryWhenNotFound) {
ASSERT_TRUE(embedded_test_server()->Start());
AddTab(browser());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
std::unique_ptr<WebContents> wc =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(SessionID::NewUnique(), std::move(wc),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
SessionID id = SessionID::NewUnique();
EXPECT_EQ(cache.RestoreEntry(id), nullptr);
EXPECT_EQ(closed_tab_cache().RestoreEntry(id), nullptr);
}
// Restore an entry that is in the cache.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, RestoreEntryWhenFound) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, RestoreEntryWhenFound) {
ASSERT_TRUE(embedded_test_server()->Start());
AddTab(browser());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
std::unique_ptr<WebContents> wc =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
SessionID id = SessionID::NewUnique();
cache.StoreEntry(id, std::move(wc), base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
WebContents* wc = browser()->tab_strip_model()->GetWebContentsAt(1);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
EXPECT_NE(cache.RestoreEntry(id), nullptr);
RestoreMostRecentTab();
EXPECT_EQ(closed_tab_cache().EntriesCount(), 0U);
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(1), wc);
}
// Evict an entry after timeout.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, EvictEntryOnTimeout) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTest, EvictEntryOnTimeout) {
scoped_refptr<base::TestMockTimeTaskRunner> task_runner =
base::MakeRefCounted<base::TestMockTimeTaskRunner>();
cache.SetTaskRunnerForTesting(task_runner);
closed_tab_cache().SetTaskRunnerForTesting(task_runner);
AddTab(browser());
ASSERT_TRUE(embedded_test_server()->Start());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
std::unique_ptr<WebContents> wc =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(SessionID::NewUnique(), std::move(wc),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
// Fast forward to just before eviction is due.
base::TimeDelta delta = base::TimeDelta::FromMilliseconds(1);
@ -191,67 +200,103 @@ IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, EvictEntryOnTimeout) {
task_runner->FastForwardBy(ttl - delta);
// Expect the entry to still be in the cache.
EXPECT_EQ(cache.EntriesCount(), 1U);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
// Fast forward to when eviction is due.
task_runner->FastForwardBy(delta);
// Expect the entry to have been evicted.
EXPECT_EQ(cache.EntriesCount(), 0U);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 0U);
}
// Check that the cache is cleared if the memory pressure level is critical and
// the threshold is critical.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, MemoryPressureLevelCritical) {
ClosedTabCache cache;
// Test for functionality of memory pressure in closed tab cache.
class ClosedTabCacheBrowserTestWithMemoryPressure
: public ClosedTabCacheBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
ClosedTabCacheBrowserTest::SetUpCommandLine(command_line);
AddTab(browser());
scoped_feature_list_.InitWithFeaturesAndParameters(
{{kClosedTabCacheMemoryPressure, {}}}, {});
}
void SetMemoryPressure(
util::test::FakeMemoryPressureMonitor::MemoryPressureLevel level) {
fake_memory_pressure_monitor_.SetAndNotifyMemoryPressure(level);
// Wait for all the pressure callbacks to be run.
base::RunLoop().RunUntilIdle();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Check that no WebContents reaches the cache if the memory pressure level is
// critical and the threshold is critical.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTestWithMemoryPressure,
MemoryPressureLevelCritical) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
std::unique_ptr<WebContents> wc =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(SessionID::NewUnique(), std::move(wc),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
fake_memory_pressure_monitor_.SetAndNotifyMemoryPressure(
SetMemoryPressure(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL);
// Wait for all the pressure callbacks to be run.
base::RunLoop().RunUntilIdle();
// Expect the cache to have been cleared since the memory pressure level is
// at the threshold.
EXPECT_EQ(cache.EntriesCount(), 0U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 0U);
}
// Check that the cache is not cleared if the memory pressure level is moderate
// and the threshold is critical.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheTest, MemoryPressureLevelModerate) {
ClosedTabCache cache;
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTestWithMemoryPressure,
MemoryPressureLevelModerate) {
ASSERT_TRUE(embedded_test_server()->Start());
AddTab(browser());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
std::unique_ptr<WebContents> wc =
browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0);
ASSERT_TRUE(cache.IsEmpty())
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
cache.StoreEntry(SessionID::NewUnique(), std::move(wc),
base::TimeTicks::Now());
EXPECT_EQ(cache.EntriesCount(), 1U);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
fake_memory_pressure_monitor_.SetAndNotifyMemoryPressure(
SetMemoryPressure(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
base::RunLoop().RunUntilIdle();
// Expect the cache to not have been cleared since the memory pressure level
// is below the threshold.
EXPECT_EQ(cache.EntriesCount(), 1U);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
}
// Check that a WebContents reaches the cache if the memory pressure level is
// critical and the threshold is moderate, but gets flushed once the threshold
// reaches critical.
IN_PROC_BROWSER_TEST_F(ClosedTabCacheBrowserTestWithMemoryPressure,
MemoryPressureLevelModerateThenCritical) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigateToURL(browser(), "a.com");
ASSERT_EQ(browser()->tab_strip_model()->count(), 2);
ASSERT_TRUE(closed_tab_cache().IsEmpty())
<< "Expected cache to be empty at the start of the test.";
SetMemoryPressure(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
CloseTabAt(1);
EXPECT_EQ(closed_tab_cache().EntriesCount(), 1U);
SetMemoryPressure(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL);
// Expect the cache to have been cleared since the memory pressure level is
// at the threshold.
EXPECT_EQ(closed_tab_cache().EntriesCount(), 0U);
}

@ -5,12 +5,17 @@
#include "chrome/browser/sessions/closed_tab_cache_service_factory.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
ClosedTabCacheServiceFactory::ClosedTabCacheServiceFactory()
: BrowserContextKeyedServiceFactory(
"ClosedTabCacheService",
BrowserContextDependencyManager::GetInstance()) {}
BrowserContextDependencyManager::GetInstance()) {
DependsOn(HostContentSettingsMapFactory::GetInstance());
DependsOn(HistoryServiceFactory::GetInstance());
}
// static
ClosedTabCacheService* ClosedTabCacheServiceFactory::GetForProfile(

@ -10,10 +10,14 @@
#include "base/feature_list.h"
#include "base/token.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/closed_tab_cache.h"
#include "chrome/browser/sessions/closed_tab_cache_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_tab_strip_model_delegate.h"
#include "chrome/browser/ui/browser_tabrestore.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_group.h"
@ -152,10 +156,32 @@ sessions::LiveTab* BrowserLiveTabContext::AddRestoredTab(
const bool first_tab_in_group =
group.has_value() ? !group_model->ContainsTabGroup(group.value()) : false;
WebContents* web_contents = chrome::AddRestoredTab(
browser_, navigations, tab_index, selected_navigation, extension_app_id,
group, select, pin, base::TimeTicks(), storage_namespace,
user_agent_override, false /* from_session_restore */);
bool restored_from_closed_tab_cache = false;
WebContents* web_contents = nullptr;
if (tab_id) {
// Try to restore the WebContents from the ClosedTabCache rather than
// creating it again.
ClosedTabCache& cache =
ClosedTabCacheServiceFactory::GetForProfile(browser_->profile())
->closed_tab_cache();
std::unique_ptr<WebContents> wc = cache.RestoreEntry(*tab_id);
if (wc) {
// Cache hit.
restored_from_closed_tab_cache = true;
web_contents = chrome::AddRestoredTabFromCache(std::move(wc), browser_,
tab_index, group, select,
pin, user_agent_override);
}
}
if (!restored_from_closed_tab_cache) {
// Cache miss, ClosedTabCache feature disabled or non-existent |tab_id|.
web_contents = chrome::AddRestoredTab(
browser_, navigations, tab_index, selected_navigation, extension_app_id,
group, select, pin, base::TimeTicks(), storage_namespace,
user_agent_override, false /* from_session_restore */);
}
// Only update the metadata if the group doesn't already exist since the
// existing group has the latest metadata, which may have changed from the
@ -166,23 +192,25 @@ sessions::LiveTab* BrowserLiveTabContext::AddRestoredTab(
group_model->GetTabGroup(group.value())->SetVisualData(new_data);
}
if (!restored_from_closed_tab_cache) {
#if BUILDFLAG(ENABLE_SESSION_SERVICE)
// The focused tab will be loaded by Browser, and TabLoader will load the
// rest.
if (!select) {
// Regression check: make sure that the tab hasn't started to load
// immediately.
DCHECK(web_contents->GetController().NeedsReload());
DCHECK(!web_contents->IsLoading());
}
std::vector<TabLoader::RestoredTab> restored_tabs;
restored_tabs.emplace_back(web_contents, select, !extension_app_id.empty(),
pin, group);
TabLoader::RestoreTabs(restored_tabs, base::TimeTicks::Now());
// The focused tab will be loaded by Browser, and TabLoader will load the
// rest.
if (!select) {
// Regression check: make sure that the tab hasn't started to load
// immediately.
DCHECK(web_contents->GetController().NeedsReload());
DCHECK(!web_contents->IsLoading());
}
std::vector<TabLoader::RestoredTab> restored_tabs;
restored_tabs.emplace_back(web_contents, select, !extension_app_id.empty(),
pin, group);
TabLoader::RestoreTabs(restored_tabs, base::TimeTicks::Now());
#else // BUILDFLAG(ENABLE_SESSION_SERVICE)
// Load the tab manually if there is no TabLoader.
web_contents->GetController().LoadIfNecessary();
// Load the tab manually if there is no TabLoader.
web_contents->GetController().LoadIfNecessary();
#endif // BUILDFLAG(ENABLE_SESSION_SERVICE)
}
return sessions::ContentLiveTab::GetForWebContents(web_contents);
}

@ -8,7 +8,11 @@
#include "base/bind.h"
#include "base/command_line.h"
#include "build/build_config.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/closed_tab_cache.h"
#include "chrome/browser/sessions/closed_tab_cache_service_factory.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/task_manager/web_contents_tags.h"
#include "chrome/browser/ui/browser.h"
@ -228,6 +232,33 @@ void BrowserTabStripModelDelegate::AddToReadLater(
chrome::MoveTabToReadLater(browser_, web_contents);
}
void BrowserTabStripModelDelegate::CacheWebContents(
const std::vector<std::unique_ptr<TabStripModel::DetachedWebContents>>&
web_contents) {
if (browser_shutdown::HasShutdownStarted() ||
browser_->profile()->IsOffTheRecord() ||
!ClosedTabCache::IsFeatureEnabled()) {
return;
}
ClosedTabCache& cache =
ClosedTabCacheServiceFactory::GetForProfile(browser_->profile())
->closed_tab_cache();
// We assume a cache size of one. Only the last recently closed tab will be
// cached.
// TODO(https://crbug.com/1236077): Cache more than one tab in ClosedTabCache.
auto& dwc = web_contents.back();
if (!cache.CanCacheWebContents(dwc->id))
return;
std::unique_ptr<content::WebContents> wc;
dwc->owned_contents.swap(wc);
dwc->remove_reason = TabStripModelChange::RemoveReason::kCached;
auto cached = std::make_pair(dwc->id, std::move(wc));
cache.CacheWebContents(std::move(cached));
}
////////////////////////////////////////////////////////////////////////////////
// BrowserTabStripModelDelegate, private:

@ -52,6 +52,9 @@ class BrowserTabStripModelDelegate : public TabStripModelDelegate {
bool ShouldDisplayFavicon(content::WebContents* contents) const override;
bool CanReload() const override;
void AddToReadLater(content::WebContents* web_contents) override;
void CacheWebContents(
const std::vector<std::unique_ptr<TabStripModel::DetachedWebContents>>&
web_contents) override;
void CloseFrame();

@ -40,6 +40,10 @@ namespace chrome {
namespace {
// TODO(https://crbug.com/1119368): Consider making CreateRestoredTab public and
// separate AddRestoredTab from CreateRestoredTab to distinguish the cases where
// a tab doesn't need to be created when it can be restored from the cache. At
// that point, there would be no need for the AddRestoredTabFromCache method.
std::unique_ptr<WebContents> CreateRestoredTab(
Browser* browser,
const std::vector<SerializedNavigationEntry>& navigations,
@ -115,27 +119,13 @@ void LoadRestoredTabIfVisible(Browser* browser,
web_contents->GetController().LoadIfNecessary();
}
} // namespace
WebContents* AddRestoredTab(
Browser* browser,
const std::vector<SerializedNavigationEntry>& navigations,
int tab_index,
int selected_navigation,
const std::string& extension_app_id,
absl::optional<tab_groups::TabGroupId> group,
bool select,
bool pin,
base::TimeTicks last_active_time,
content::SessionStorageNamespace* session_storage_namespace,
const sessions::SerializedUserAgentOverride& user_agent_override,
bool from_session_restore) {
const bool initially_hidden = !select || browser->window()->IsMinimized();
std::unique_ptr<WebContents> web_contents = CreateRestoredTab(
browser, navigations, selected_navigation, extension_app_id,
last_active_time, session_storage_namespace, user_agent_override,
initially_hidden, from_session_restore);
WebContents* AddRestoredTabImpl(std::unique_ptr<WebContents> web_contents,
Browser* browser,
int tab_index,
absl::optional<tab_groups::TabGroupId> group,
bool select,
bool pin,
bool from_session_restore) {
TabStripModel* const tab_strip_model = browser->tab_strip_model();
int add_types = select ? TabStripModel::ADD_ACTIVE : TabStripModel::ADD_NONE;
@ -165,6 +155,7 @@ WebContents* AddRestoredTab(
tab_strip_model->AddToGroupForRestore({actual_index}, group.value());
}
const bool initially_hidden = !select || browser->window()->IsMinimized();
if (initially_hidden) {
// We set the size of the view here, before Blink does its initial layout.
// If we don't, the initial layout of background tabs will be performed
@ -214,6 +205,52 @@ WebContents* AddRestoredTab(
return raw_web_contents;
}
} // namespace
WebContents* AddRestoredTab(
Browser* browser,
const std::vector<SerializedNavigationEntry>& navigations,
int tab_index,
int selected_navigation,
const std::string& extension_app_id,
absl::optional<tab_groups::TabGroupId> group,
bool select,
bool pin,
base::TimeTicks last_active_time,
content::SessionStorageNamespace* session_storage_namespace,
const sessions::SerializedUserAgentOverride& user_agent_override,
bool from_session_restore) {
const bool initially_hidden = !select || browser->window()->IsMinimized();
std::unique_ptr<WebContents> web_contents = CreateRestoredTab(
browser, navigations, selected_navigation, extension_app_id,
last_active_time, session_storage_namespace, user_agent_override,
initially_hidden, from_session_restore);
return AddRestoredTabImpl(std::move(web_contents), browser, tab_index, group,
select, pin, from_session_restore);
}
WebContents* AddRestoredTabFromCache(
std::unique_ptr<WebContents> web_contents,
Browser* browser,
int tab_index,
absl::optional<tab_groups::TabGroupId> group,
bool select,
bool pin,
const sessions::SerializedUserAgentOverride& user_agent_override) {
// TODO(crbug.com/1227397): Check whether |ua_override| has changed for the
// tab we're trying to restore from ClosedTabCache. Don't restore if the
// values differ.
blink::UserAgentOverride ua_override;
ua_override.ua_string_override = user_agent_override.ua_string_override;
ua_override.ua_metadata_override = blink::UserAgentMetadata::Demarshal(
user_agent_override.opaque_ua_metadata_override);
web_contents->SetUserAgentOverride(ua_override, false);
return AddRestoredTabImpl(std::move(web_contents), browser, tab_index, group,
select, pin, /*from_session_restore=*/false);
}
WebContents* ReplaceRestoredTab(
Browser* browser,
const std::vector<SerializedNavigationEntry>& navigations,

@ -53,6 +53,19 @@ content::WebContents* AddRestoredTab(
const sessions::SerializedUserAgentOverride& user_agent_override,
bool from_session_restore);
// Same functionality as AddRestoreTab, except that the |web_contents| is
// passed as it was never deleted. Used when restoring entry from
// ClosedTabCache. Note that ClosedTabCache is an experimental desktop feature
// to instantly restore recently closed tabs.
content::WebContents* AddRestoredTabFromCache(
std::unique_ptr<content::WebContents> web_contents,
Browser* browser,
int tab_index,
absl::optional<tab_groups::TabGroupId> group,
bool select,
bool pin,
const sessions::SerializedUserAgentOverride& user_agent_override);
// Replaces the state of the currently selected tab with the session
// history restored from the SessionRestore and TabRestoreService systems.
// Returns the WebContents of the restored tab.

@ -5,6 +5,7 @@
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <utility>
@ -259,36 +260,22 @@ void TabStripModel::WebContentsData::WebContentsDestroyed() {
NOTREACHED();
}
// Holds state for a WebContents that has been detached from the tab strip. Will
// also handle WebContents deletion if |remove_reason| is kDeleted.
struct TabStripModel::DetachedWebContents {
DetachedWebContents(int index_before_any_removals,
int index_at_time_of_removal,
std::unique_ptr<WebContents> contents,
TabStripModelChange::RemoveReason remove_reason)
: contents(std::move(contents)),
index_before_any_removals(index_before_any_removals),
index_at_time_of_removal(index_at_time_of_removal),
remove_reason(remove_reason) {}
DetachedWebContents(const DetachedWebContents&) = delete;
DetachedWebContents& operator=(const DetachedWebContents&) = delete;
~DetachedWebContents() = default;
DetachedWebContents(DetachedWebContents&&) = default;
std::unique_ptr<WebContents> contents;
// The index of the WebContents in the original selection model of the tab
// strip [prior to any tabs being removed, if multiple tabs are being
// simultaneously removed].
const int index_before_any_removals;
// The index of the WebContents at the time it is being removed. If multiple
// tabs are being simultaneously removed, the index reflects previously
// removed tabs in this batch.
const int index_at_time_of_removal;
const TabStripModelChange::RemoveReason remove_reason;
};
TabStripModel::DetachedWebContents::DetachedWebContents(
int index_before_any_removals,
int index_at_time_of_removal,
std::unique_ptr<WebContents> owned_contents,
content::WebContents* contents,
TabStripModelChange::RemoveReason remove_reason,
absl::optional<SessionID> id)
: owned_contents(std::move(owned_contents)),
contents(contents),
index_before_any_removals(index_before_any_removals),
index_at_time_of_removal(index_at_time_of_removal),
remove_reason(remove_reason),
id(id) {}
TabStripModel::DetachedWebContents::~DetachedWebContents() = default;
TabStripModel::DetachedWebContents::DetachedWebContents(DetachedWebContents&&) =
default;
// Holds all state necessary to send notifications for detached tabs.
struct TabStripModel::DetachNotifications {
@ -300,7 +287,8 @@ struct TabStripModel::DetachNotifications {
DetachNotifications& operator=(const DetachNotifications&) = delete;
~DetachNotifications() = default;
// The WebContents that was active prior to any detaches happening.
// The WebContents that was active prior to any detaches happening. If this
// is nullptr, the active WebContents was not removed.
//
// It's safe to use a raw pointer here because the active web contents, if
// detached, is owned by |detached_web_contents|.
@ -430,8 +418,9 @@ std::unique_ptr<content::WebContents> TabStripModel::ReplaceWebContentsAt(
std::unique_ptr<content::WebContents>
TabStripModel::DetachWebContentsAtForInsertion(int index) {
return DetachWebContentsWithReasonAt(
auto dwc = DetachWebContentsWithReasonAt(
index, TabStripModelChange::RemoveReason::kInsertedIntoOtherTabStrip);
return std::move(dwc->owned_contents);
}
void TabStripModel::DetachAndDeleteWebContentsAt(int index) {
@ -440,7 +429,7 @@ void TabStripModel::DetachAndDeleteWebContentsAt(int index) {
TabStripModelChange::RemoveReason::kDeleted);
}
std::unique_ptr<content::WebContents>
std::unique_ptr<TabStripModel::DetachedWebContents>
TabStripModel::DetachWebContentsWithReasonAt(
int index,
TabStripModelChange::RemoveReason reason) {
@ -454,47 +443,49 @@ TabStripModel::DetachWebContentsWithReasonAt(
DetachNotifications notifications(initially_active_web_contents,
selection_model_);
std::unique_ptr<DetachedWebContents> dwc =
std::make_unique<DetachedWebContents>(
index, index,
DetachWebContentsImpl(index, /*create_historical_tab=*/false),
reason);
auto dwc = DetachWebContentsImpl(index, index,
/*create_historical_tab=*/false, reason);
notifications.detached_web_contents.push_back(std::move(dwc));
SendDetachWebContentsNotifications(&notifications);
return std::move(notifications.detached_web_contents[0]->contents);
return std::move(notifications.detached_web_contents[0]);
}
std::unique_ptr<content::WebContents> TabStripModel::DetachWebContentsImpl(
int index,
bool create_historical_tab) {
std::unique_ptr<TabStripModel::DetachedWebContents>
TabStripModel::DetachWebContentsImpl(int index_before_any_removals,
int index_at_time_of_removal,
bool create_historical_tab,
TabStripModelChange::RemoveReason reason) {
if (contents_data_.empty())
return nullptr;
CHECK(ContainsIndex(index));
CHECK(ContainsIndex(index_at_time_of_removal));
FixOpeners(index);
FixOpeners(index_at_time_of_removal);
// Ask the delegate to save an entry for this tab in the historical tab
// database.
WebContents* raw_web_contents = GetWebContentsAtImpl(index);
WebContents* raw_web_contents =
GetWebContentsAtImpl(index_at_time_of_removal);
absl::optional<SessionID> id = absl::nullopt;
if (create_historical_tab)
delegate_->CreateHistoricalTab(raw_web_contents);
id = delegate_->CreateHistoricalTab(raw_web_contents);
absl::optional<int> next_selected_index =
order_controller_->DetermineNewSelectedIndex(index);
order_controller_->DetermineNewSelectedIndex(index_at_time_of_removal);
UngroupTab(index);
UngroupTab(index_at_time_of_removal);
std::unique_ptr<WebContentsData> old_data = std::move(contents_data_[index]);
contents_data_.erase(contents_data_.begin() + index);
std::unique_ptr<WebContentsData> old_data =
std::move(contents_data_[index_at_time_of_removal]);
contents_data_.erase(contents_data_.begin() + index_at_time_of_removal);
if (empty()) {
selection_model_.Clear();
} else {
int old_active = active_index();
selection_model_.DecrementFrom(index);
selection_model_.DecrementFrom(index_at_time_of_removal);
ui::ListSelectionModel old_model;
old_model = selection_model_;
if (index == old_active) {
if (index_at_time_of_removal == old_active) {
if (!selection_model_.empty()) {
// The active tab was removed, but there is still something selected.
// Move the active and anchor to the first selected index.
@ -509,7 +500,12 @@ std::unique_ptr<content::WebContents> TabStripModel::DetachWebContentsImpl(
}
}
}
return old_data->ReplaceWebContents(nullptr);
auto owned_contents = old_data->ReplaceWebContents(nullptr);
auto* contents = owned_contents.get();
return std::make_unique<DetachedWebContents>(
index_before_any_removals, index_at_time_of_removal,
std::move(owned_contents), contents, reason, id);
}
void TabStripModel::SendDetachWebContentsNotifications(
@ -528,9 +524,8 @@ void TabStripModel::SendDetachWebContentsNotifications(
TabStripModelChange::Remove remove;
for (auto& dwc : notifications->detached_web_contents) {
remove.contents.push_back({dwc->contents.get(),
dwc->index_before_any_removals,
dwc->remove_reason});
remove.contents.push_back(
{dwc->contents, dwc->index_before_any_removals, dwc->remove_reason});
}
TabStripModelChange change(std::move(remove));
@ -558,7 +553,8 @@ void TabStripModel::SendDetachWebContentsNotifications(
if (dwc->remove_reason == TabStripModelChange::RemoveReason::kDeleted) {
// This destroys the WebContents, which will also send
// WebContentsDestroyed notifications.
dwc->contents.reset();
dwc->owned_contents.reset();
dwc->contents = nullptr;
}
}
@ -1847,6 +1843,7 @@ bool TabStripModel::CloseWebContentses(
for (size_t i = 0; i < items.size(); ++i)
original_indices[i] = GetIndexOfWebContents(items[i]);
std::vector<std::unique_ptr<DetachedWebContents>> detached_web_contents;
for (size_t i = 0; i < items.size(); ++i) {
WebContents* closing_contents = items[i];
@ -1868,15 +1865,30 @@ bool TabStripModel::CloseWebContentses(
continue;
}
std::unique_ptr<DetachedWebContents> dwc =
std::make_unique<DetachedWebContents>(
original_indices[i], current_index,
DetachWebContentsImpl(current_index,
close_types & CLOSE_CREATE_HISTORICAL_TAB),
TabStripModelChange::RemoveReason::kDeleted);
notifications->detached_web_contents.push_back(std::move(dwc));
bool create_historical_tab = close_types & CLOSE_CREATE_HISTORICAL_TAB;
auto dwc = DetachWebContentsImpl(
original_indices[i], current_index, create_historical_tab,
TabStripModelChange::RemoveReason::kDeleted);
detached_web_contents.push_back(std::move(dwc));
}
// ClosedTabCache will only take ownership of the last recently closed tab.
delegate_->CacheWebContents(detached_web_contents);
// If the delegate takes ownership, it must reset the reason.
#if DCHECK_IS_ON()
for (auto& dwc : detached_web_contents)
if (dwc->owned_contents) {
DCHECK_EQ(TabStripModelChange::RemoveReason::kDeleted,
dwc->remove_reason);
} else {
DCHECK_EQ(TabStripModelChange::RemoveReason::kCached, dwc->remove_reason);
}
#endif
for (auto& dwc : detached_web_contents)
notifications->detached_web_contents.push_back(std::move(dwc));
return closed_all;
}

@ -26,6 +26,7 @@
#include "chrome/browser/ui/tabs/tab_group_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model_order_controller.h"
#include "chrome/browser/ui/tabs/tab_switch_event_latency_recorder.h"
#include "components/sessions/core/session_id.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
@ -153,6 +154,49 @@ class TabStripModel : public TabGroupController {
kMaxValue = kContextMenu,
};
// Holds state for a WebContents that has been detached from the tab strip.
// Will also handle WebContents deletion if |remove_reason| is kDeleted, or
// WebContents caching if |remove_reason| is kCached.
// TODO(https://crbug.com/1234327): Don't make DetachedWebContents an inner
// class, so it can be forward declared in TabStripModelDelegate.
struct DetachedWebContents {
DetachedWebContents(int index_before_any_removals,
int index_at_time_of_removal,
std::unique_ptr<content::WebContents> owned_contents,
content::WebContents* contents,
TabStripModelChange::RemoveReason remove_reason,
absl::optional<SessionID> id);
DetachedWebContents(const DetachedWebContents&) = delete;
DetachedWebContents& operator=(const DetachedWebContents&) = delete;
~DetachedWebContents();
DetachedWebContents(DetachedWebContents&&);
// When a WebContents is removed the delegate is given a chance to
// take ownership of it (generally for caching). If the delegate takes
// ownership, `owned_contents` will be null, and `contents` will be
// non-null. In other words, all observers should use `contents`, it is
// guaranteed to be valid for the life time of the notification (and
// possibly longer).
std::unique_ptr<content::WebContents> owned_contents;
content::WebContents* contents;
// The index of the WebContents in the original selection model of the tab
// strip [prior to any tabs being removed, if multiple tabs are being
// simultaneously removed].
const int index_before_any_removals;
// The index of the WebContents at the time it is being removed. If multiple
// tabs are being simultaneously removed, the index reflects previously
// removed tabs in this batch.
const int index_at_time_of_removal;
TabStripModelChange::RemoveReason remove_reason;
// The |contents| associated optional SessionID, used as key for
// ClosedTabCache. We only cache |contents| if |remove_reason| is kCached.
absl::optional<SessionID> id;
};
static constexpr int kNoTab = -1;
// Construct a TabStripModel with a delegate to help it do certain things
@ -603,24 +647,25 @@ class TabStripModel : public TabGroupController {
FRIEND_TEST_ALL_PREFIXES(TabStripModelTest, GetIndicesClosedByCommand);
class WebContentsData;
struct DetachedWebContents;
struct DetachNotifications;
// Detaches the WebContents at the specified |index| from this strip. |reason|
// is used to indicate to observers what is going to happen to the WebContents
// (i.e. deleted or reinserted into another tab strip). Returns the detached
// WebContents.
std::unique_ptr<content::WebContents> DetachWebContentsWithReasonAt(
int index,
TabStripModelChange::RemoveReason reason);
std::unique_ptr<TabStripModel::DetachedWebContents>
DetachWebContentsWithReasonAt(int index,
TabStripModelChange::RemoveReason reason);
// Performs all the work to detach a WebContents instance but avoids sending
// most notifications. TabClosingAt() and TabDetachedAt() are sent because
// observers are reliant on the selection model being accurate at the time
// that TabDetachedAt() is called.
std::unique_ptr<content::WebContents> DetachWebContentsImpl(
int index,
bool create_historical_tab);
std::unique_ptr<DetachedWebContents> DetachWebContentsImpl(
int index_before_any_removals,
int index_at_time_of_removal,
bool create_historical_tab,
TabStripModelChange::RemoveReason reason);
// We batch send notifications. This has two benefits:
// 1) This allows us to send the minimal number of necessary notifications.

@ -8,6 +8,7 @@
#include <memory>
#include <vector>
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "components/sessions/core/session_id.h"
#include "components/tab_groups/tab_group_id.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
@ -151,6 +152,18 @@ class TabStripModelDelegate {
// Adds the specified WebContents to read later.
virtual void AddToReadLater(content::WebContents* web_contents) = 0;
// Gives the delegate an opportunity to cache (take ownership) of
// WebContents before they are destroyed. The delegate takes ownership by way
// of using std::move() on the `owned_contents` and resetting `remove_reason`
// to kCached. It is expected that any WebContents the delegate takes
// ownership of remain valid until the next message is pumped. In other
// words, the delegate must not immediately destroy any of the supplied
// WebContents.
// TODO(https://crbug.com/1234332): Provide active web contents.
virtual void CacheWebContents(
const std::vector<std::unique_ptr<TabStripModel::DetachedWebContents>>&
web_contents) = 0;
};
#endif // CHROME_BROWSER_UI_TABS_TAB_STRIP_MODEL_DELEGATE_H_

@ -97,3 +97,7 @@ bool TestTabStripModelDelegate::CanReload() const {
void TestTabStripModelDelegate::AddToReadLater(
content::WebContents* web_contents) {}
void TestTabStripModelDelegate::CacheWebContents(
const std::vector<std::unique_ptr<TabStripModel::DetachedWebContents>>&
web_contents) {}

@ -51,6 +51,9 @@ class TestTabStripModelDelegate : public TabStripModelDelegate {
bool ShouldDisplayFavicon(content::WebContents* web_contents) const override;
bool CanReload() const override;
void AddToReadLater(content::WebContents* web_contents) override;
void CacheWebContents(
const std::vector<std::unique_ptr<TabStripModel::DetachedWebContents>>&
web_contents) override;
};
#endif // CHROME_BROWSER_UI_TABS_TEST_TAB_STRIP_MODEL_DELEGATE_H_