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:

committed by
Chromium LUCI CQ

parent
d7e577453b
commit
08280881f0
1
AUTHORS
1
AUTHORS
@ -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(¬ifications);
|
||||
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_
|
||||
|
Reference in New Issue
Block a user