| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/sessions/tab_loader.h" |
| |
| #include <vector> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/resource_coordinator/tab_helper.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_features.h" |
| #include "chrome/browser/sessions/session_restore_test_utils.h" |
| #include "chrome/browser/sessions/tab_loader_tester.h" |
| #include "chrome/browser/ui/tabs/tab_model.h" |
| #include "chrome/test/base/browser_with_test_window_test.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/test_web_contents_factory.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using resource_coordinator::TabLoadTracker; |
| using resource_coordinator::ResourceCoordinatorTabHelper; |
| using LoadingState = TabLoadTracker::LoadingState; |
| |
| class TabLoaderTest : public BrowserWithTestWindowTest { |
| protected: |
| using Super = BrowserWithTestWindowTest; |
| using RestoredTab = SessionRestoreDelegate::RestoredTab; |
| |
| TabLoaderTest() : max_simultaneous_loads_(1) {} |
| |
| TabLoaderTest(const TabLoaderTest&) = delete; |
| TabLoaderTest& operator=(const TabLoaderTest&) = delete; |
| |
| void OnTabLoaderCreated(TabLoader* tab_loader) { |
| tab_loader_.SetTabLoader(tab_loader); |
| tab_loader_.SetTickClockForTesting(&clock_); |
| if (max_simultaneous_loads_ != 0) |
| tab_loader_.SetMaxSimultaneousLoadsForTesting(max_simultaneous_loads_); |
| } |
| |
| // testing::Test: |
| void SetUp() override { |
| Super::SetUp(); |
| construction_callback_ = base::BindRepeating( |
| &TabLoaderTest::OnTabLoaderCreated, base::Unretained(this)); |
| TabLoaderTester::SetConstructionCallbackForTesting(&construction_callback_); |
| test_policy_ = |
| std::make_unique<testing::ScopedAlwaysLoadSessionRestoreTestPolicy>(); |
| } |
| |
| void TearDown() override { |
| if (TabLoaderTester::shared_tab_loader() != nullptr) { |
| // Expect the TabLoader to detach after all tabs have loaded. |
| SimulateLoadedAll(); |
| EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr); |
| } |
| |
| TabLoaderTester::SetConstructionCallbackForTesting(nullptr); |
| task_environment()->RunUntilIdle(); |
| test_policy_.reset(); |
| Super::TearDown(); |
| } |
| |
| void SimulateLoadTimeout() { |
| // Unfortunately there's no mock time in BrowserTaskEnvironment. |
| // Fast-forward things and simulate firing the timer. |
| // TODO(crbug.com/40602467): TaskEnvironment::TimeSource::MOCK_TIME now |
| // supports this. |
| EXPECT_TRUE(tab_loader_.force_load_timer().IsRunning()); |
| clock_.SetNowTicks(tab_loader_.force_load_time()); |
| tab_loader_.force_load_timer().Stop(); |
| tab_loader_.ForceLoadTimerFired(); |
| SimulatePrimaryPageChangedIfNecessary(); |
| } |
| |
| void SimulateStartedToLoad(size_t tab_index) { |
| auto* contents = restored_tabs_[tab_index].contents(); |
| auto* tracker = TabLoadTracker::Get(); |
| tracker->TransitionStateForTesting(contents, LoadingState::LOADING); |
| SimulatePrimaryPageChangedIfNecessary(); |
| } |
| |
| void SimulateLoaded(size_t tab_index) { |
| // Transition to a LOADED state. This has to pass through the LOADING state |
| // in order to satisfy the internal logic of SessionRestoreStatsCollector. |
| auto* contents = restored_tabs_[tab_index].contents(); |
| auto* tracker = TabLoadTracker::Get(); |
| if (tracker->GetLoadingState(contents) != LoadingState::LOADING) |
| tracker->TransitionStateForTesting(contents, LoadingState::LOADING); |
| tracker->TransitionStateForTesting(contents, LoadingState::LOADED); |
| SimulatePrimaryPageChangedIfNecessary(); |
| } |
| |
| void SimulateLoadedAll() { |
| for (size_t i = 0; i < restored_tabs_.size(); ++i) |
| SimulateLoaded(i); |
| } |
| |
| content::WebContents* CreateRestoredWebContents(bool is_active) { |
| std::unique_ptr<content::WebContents> test_contents = |
| content::WebContentsTester::CreateTestWebContents( |
| profile(), content::SiteInstance::Create(profile())); |
| auto* raw_contents = test_contents.get(); |
| std::vector<std::unique_ptr<content::NavigationEntry>> entries; |
| entries.push_back(content::NavigationEntry::Create()); |
| test_contents->GetController().Restore(0, content::RestoreType::kRestored, |
| &entries); |
| // TabLoadTracker needs the resource_coordinator WebContentsData to be |
| // initialized. |
| ResourceCoordinatorTabHelper::CreateForWebContents(raw_contents); |
| restored_tabs_.push_back( |
| RestoredTab(raw_contents, is_active /* is_active */, false /* is_app */, |
| false /* is_pinned */, std::nullopt /* group */, |
| std::nullopt /* split */)); |
| |
| // Add the contents to the tab strip model, which becomes the owner. |
| auto* tab_strip_model = browser()->tab_strip_model(); |
| tab_strip_model->AppendWebContents(std::move(test_contents), is_active); |
| |
| if (is_active) { |
| // If the tab is active start "loading" it right away for consistency with |
| // session restore code. |
| raw_contents->GetController().LoadIfNecessary(); |
| } |
| |
| return raw_contents; |
| } |
| |
| void CreateMultipleRestoredWebContents(size_t num_active, |
| size_t num_inactive) { |
| // At least one active tab must be created. |
| DCHECK_LT(0u, num_active); |
| for (size_t i = 0; i < num_active; ++i) |
| CreateRestoredWebContents(true); |
| for (size_t i = 0; i < num_inactive; ++i) |
| CreateRestoredWebContents(false); |
| } |
| |
| // Since it couldn't get PrimaryPageChanged() by loading, it simulates |
| // PrimaryPageChanged() to update the status. |
| void SimulatePrimaryPageChanged(content::WebContents* web_contents) { |
| auto* helper = ResourceCoordinatorTabHelper::FromWebContents(web_contents); |
| helper->PrimaryPageChanged(web_contents->GetPrimaryPage()); |
| } |
| |
| // If the tab initiates loading, TransitionState is updated by |
| // PrimaryPageChanged() in a normal browser flow. Since this is a unit test, |
| // we simulate SimulatePrimaryPageChanged() for the loading initiated tabs. |
| void SimulatePrimaryPageChangedIfNecessary() { |
| if (!TabLoaderTester::shared_tab_loader()) |
| return; |
| |
| // Copy because the set can change while calling |
| // SimulatePrimaryPageChanged() and the iteration is invalidated. |
| base::flat_set<raw_ptr<content::WebContents, CtnExperimental>> |
| load_initiated = tab_loader_.tabs_load_initiated(); |
| for (content::WebContents* web_contents : load_initiated) { |
| SimulatePrimaryPageChanged(web_contents); |
| } |
| } |
| |
| void StartTabLoader() { |
| // Call PrimaryPageChanged() that would be caused by LoadIfNecessary() from |
| // CreateRestoredWebContents(). |
| for (auto& tab : restored_tabs_) { |
| if (tab.is_active()) |
| SimulatePrimaryPageChanged(tab.contents()); |
| } |
| |
| TabLoader::RestoreTabs(restored_tabs_, clock_.NowTicks()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| EXPECT_FALSE(tab_loader_.IsLoadingEnabled()); |
| tab_loader_.WaitForTabLoadingEnabled(); |
| } |
| |
| // The number of loading slots to use. This needs to be set before the |
| // TabLoader is created in order to be picked up by it. |
| size_t max_simultaneous_loads_; |
| |
| // Set of restored tabs that is populated by calls to |
| // CreateRestoredWebContents. |
| std::vector<RestoredTab> restored_tabs_; |
| |
| // Automatically attaches to the tab loader that is created by the test. |
| TabLoaderTester tab_loader_; |
| |
| // The tick clock that is injected into the tab loader. |
| base::SimpleTestTickClock clock_; |
| |
| // The post-construction testing seam that is invoked by TabLoader. |
| base::RepeatingCallback<void(TabLoader*)> construction_callback_; |
| |
| std::unique_ptr<testing::ScopedAlwaysLoadSessionRestoreTestPolicy> |
| test_policy_; |
| }; |
| |
| TEST_F(TabLoaderTest, AllLoadingSlotsUsed) { |
| // Create 2 active tabs and 4 inactive tabs. |
| CreateMultipleRestoredWebContents(2, 4); |
| |
| // Use 4 loading slots. The active tabs will only use 2 which means 2 of the |
| // inactive tabs should immediately be scheduled to load as well. |
| max_simultaneous_loads_ = 4; |
| |
| StartTabLoader(); |
| |
| // The loader should be enabled, with 2 tabs loading and 4 tabs left to go. |
| // The initial load should exclusively allow active tabs time to load, and |
| // fill up the rest of the loading slots. |
| EXPECT_TRUE(tab_loader_.IsLoadingEnabled()); |
| EXPECT_EQ(4u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(2u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| |
| // Trying to load another tab should do nothing as no tab has yet finished |
| // loading. |
| tab_loader_.MaybeLoadSomeTabsForTesting(); |
| EXPECT_EQ(4u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(2u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Mark an active tab as having finished loading. This marks the end of the |
| // exclusive loading period and all slots should be full now. |
| SimulateLoaded(0); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(5u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(4u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Trying to load more tabs should still do nothing. |
| tab_loader_.MaybeLoadSomeTabsForTesting(); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(5u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(4u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| } |
| |
| TEST_F(TabLoaderTest, ForceLoadTimer) { |
| // Create 1 active tab and 1 inactive tab with 1 loading slot. |
| CreateMultipleRestoredWebContents(1, 1); |
| max_simultaneous_loads_ = 1; |
| |
| StartTabLoader(); |
| |
| // The loader should be enabled, with 1 tab loading and 1 tab left to go. |
| EXPECT_TRUE(tab_loader_.IsLoadingEnabled()); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| SimulateLoadTimeout(); |
| EXPECT_FALSE(tab_loader_.HasTimedOutLoads()); |
| |
| // Expect all tabs to be loading. Note that this also validates that |
| // force-loads can exceed the number of loadingslots. |
| EXPECT_TRUE(tab_loader_.IsLoadingEnabled()); |
| EXPECT_TRUE(tab_loader_.tabs_to_load().empty()); |
| EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(2u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| } |
| |
| TEST_F(TabLoaderTest, LoadsAreStaggered) { |
| // Create 1 active tab and 1 inactive tab with 1 loading slot. |
| CreateMultipleRestoredWebContents(1, 1); |
| max_simultaneous_loads_ = 1; |
| |
| StartTabLoader(); |
| |
| // The loader should be enabled, with 1 tab loading and 1 tab left to go. |
| EXPECT_TRUE(tab_loader_.IsLoadingEnabled()); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Simulate the first tab finishing loading. |
| SimulateLoaded(0); |
| |
| // Expect all tabs to be loaded/loading. |
| EXPECT_TRUE(tab_loader_.IsLoadingEnabled()); |
| EXPECT_TRUE(tab_loader_.tabs_to_load().empty()); |
| EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadedTabCount()); |
| EXPECT_EQ(1u, TabLoadTracker::Get()->GetLoadingTabCount()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| } |
| |
| TEST_F(TabLoaderTest, OnMemoryPressure) { |
| // Multiple contents are necessary to make sure that the tab loader |
| // doesn't immediately kick off loading of all tabs and detach. |
| CreateMultipleRestoredWebContents(1, 2); |
| |
| max_simultaneous_loads_ = 1; |
| StartTabLoader(); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| |
| // Simulate memory pressure and expect the tab loader to disable loading. |
| EXPECT_TRUE(tab_loader_.IsLoadingEnabled()); |
| tab_loader_.OnMemoryPressure( |
| base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE); |
| EXPECT_FALSE(tab_loader_.IsLoadingEnabled()); |
| |
| // Finish loading the tab and expect the tab loader to disconnect. |
| SimulateLoaded(0); |
| EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr); |
| } |
| |
| TEST_F(TabLoaderTest, TimeoutCanExceedLoadingSlots) { |
| CreateMultipleRestoredWebContents(1, 4); |
| |
| // Create the tab loader with 2 loading slots. This should initially start |
| // loading 1 tab, due to exclusive initial loading of active tabs. |
| max_simultaneous_loads_ = 2; |
| StartTabLoader(); |
| EXPECT_EQ(4u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| |
| // Simulate a timeout and expect there to be 2 loading tabs and 3 left to |
| // load. |
| SimulateLoadTimeout(); |
| EXPECT_FALSE(tab_loader_.HasTimedOutLoads()); |
| EXPECT_EQ(3u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(2u, tab_loader_.force_load_delay_multiplier()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Do it again and expect 3 tabs to be loading. |
| SimulateLoadTimeout(); |
| EXPECT_FALSE(tab_loader_.HasTimedOutLoads()); |
| EXPECT_EQ(2u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(3u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(4u, tab_loader_.force_load_delay_multiplier()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Do it again and expect 4 tabs to be loading. |
| SimulateLoadTimeout(); |
| EXPECT_FALSE(tab_loader_.HasTimedOutLoads()); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(4u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_EQ(8u, tab_loader_.force_load_delay_multiplier()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Simulate the first tab finishing loading and don't expect more tabs to |
| // start loading. |
| SimulateLoaded(0); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(4u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Simulate the second tab finishing loading and don't expect more tabs to |
| // start loading. |
| SimulateLoaded(1); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(4u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Simulate the third tab finishing loading and this time expect the last tab |
| // load to be initiated. There are no tabs left so the TabLoader should also |
| // have initiated a self-destroy. |
| SimulateLoaded(2); |
| EXPECT_TRUE(tab_loader_.tabs_to_load().empty()); |
| EXPECT_EQ(5u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| } |
| |
| TEST_F(TabLoaderTest, DelegatePolicyIsApplied) { |
| namespace rc = resource_coordinator; |
| |
| test_policy_.reset(); |
| |
| // Don't directly configure the max simultaneous loads, but rather let it be |
| // configured via the policy engine. |
| max_simultaneous_loads_ = 0; |
| |
| // Create 5 tabs to restore, 1 foreground and 4 background. |
| CreateMultipleRestoredWebContents(1, 4); |
| |
| // Create the tab loader. This should initially start loading 1 tab, due to |
| // exclusive initial loading of active tabs. |
| StartTabLoader(); |
| EXPECT_EQ(4u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| |
| // Configure the policy engine explicitly. Values of zero disable those |
| // particular aspects of the policy engine. |
| auto* policy = tab_loader_.GetPolicy(); |
| policy->MinSimultaneousTabLoadsForTesting() = 2; |
| policy->MaxSimultaneousTabLoadsForTesting() = 2; |
| policy->CoresPerSimultaneousTabLoadForTesting() = 0; |
| policy->MinTabsToRestoreForTesting() = 1; |
| policy->MaxTabsToRestoreForTesting() = 3; |
| policy->MbFreeMemoryPerTabToRestoreForTesting() = 0; |
| policy->MaxTimeSinceLastUseToRestoreForTesting() = base::TimeDelta(); |
| policy->MinSiteEngagementToRestoreForTesting() = 0; |
| policy->CalculateSimultaneousTabLoadsForTesting(); |
| |
| // Simulate the first tab as having loaded. Another 2 should start loading. |
| SimulateLoaded(0); |
| EXPECT_EQ(2u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(3u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| |
| // Simulate another tab as having loaded. The last 2 tabs should be deferred |
| // (still need reloads) and the tab loader should detach. |
| SimulateLoaded(1); |
| SimulateLoaded(2); |
| EXPECT_TRUE(restored_tabs_[3].contents()->GetController().NeedsReload()); |
| EXPECT_TRUE(restored_tabs_[4].contents()->GetController().NeedsReload()); |
| EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr); |
| } |
| |
| TEST_F(TabLoaderTest, ObservesExternallyInitiatedLoads) { |
| CreateMultipleRestoredWebContents(1, 2); |
| |
| // Create the tab loader with 1 loading slots. This should initially start |
| // loading 1 tab, due to exclusive initial loading of active tabs. |
| max_simultaneous_loads_ = 1; |
| StartTabLoader(); |
| EXPECT_EQ(2u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| |
| // Manually initiate the load on one of the tabs, as would occur if a user |
| // focused a tab. The tab should no longer be in the scheduled to load bucket. |
| SimulateStartedToLoad(1); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(2u, tab_loader_.scheduled_to_load_count()); |
| EXPECT_TRUE(tab_loader_.IsSharedTabLoader()); |
| } |
| |
| TEST_F(TabLoaderTest, CloseAllTabs) { |
| CreateMultipleRestoredWebContents(1, 2); |
| |
| // Create the tab loader with 1 loading slots. This should initially start |
| // loading 1 tab, due to exclusive initial loading of active tabs. |
| max_simultaneous_loads_ = 1; |
| StartTabLoader(); |
| EXPECT_EQ(2u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| |
| // The loader should entirely disconnect when all tabs are closed. |
| browser()->tab_strip_model()->CloseAllTabs(); |
| EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr); |
| } |
| |
| TEST_F(TabLoaderTest, RemoveFromTabStrip) { |
| CreateMultipleRestoredWebContents(1, 1); |
| |
| // Create the tab loader with 1 loading slots. This should initially start |
| // loading 1 tab, due to exclusive initial loading of active tabs. |
| max_simultaneous_loads_ = 1; |
| StartTabLoader(); |
| EXPECT_EQ(1u, tab_loader_.tabs_to_load().size()); |
| EXPECT_EQ(1u, tab_loader_.scheduled_to_load_count()); |
| |
| // Remove the second tab from the tab strip model. |
| browser()->tab_strip_model()->DetachAndDeleteWebContentsAt(1); |
| |
| // The tab being removed won't be noticed by the loader until some state |
| // change it cares about occurs. Simulate the first tab finishing loading, at |
| // which point the loader should realize the other tab is no longer attached |
| // to a tab strip, and destroy itself because it has no work left to do. |
| SimulateLoaded(0); |
| EXPECT_TRUE(TabLoaderTester::shared_tab_loader() == nullptr); |
| } |