| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/resource_coordinator/tab_manager.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <set> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/feature_list.h" |
| #include "base/memory/memory_pressure_monitor.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/process/process.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/system/sys_info.h" |
| #include "base/threading/thread.h" |
| #include "base/trace_event/traced_value.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" |
| #include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" |
| #include "chrome/browser/memory/oom_memory_details.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/background_tab_navigation_throttle.h" |
| #include "chrome/browser/resource_coordinator/tab_activity_watcher.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h" |
| #include "chrome/browser/resource_coordinator/tab_manager.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_features.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_resource_coordinator_signal_observer.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_stats_collector.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_web_contents_data.h" |
| #include "chrome/browser/resource_coordinator/time.h" |
| #include "chrome/browser/sessions/session_restore.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h" |
| #include "chrome/browser/ui/tab_ui_helper.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/tabs/tab_utils.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/metrics/system_memory_stats_recorder.h" |
| #include "components/variations/variations_associated_data.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/favicon_status.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/page_importance_signals.h" |
| #include "net/base/network_change_notifier.h" |
| #include "third_party/blink/public/platform/web_sudden_termination_disabler_type.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "chrome/browser/resource_coordinator/tab_manager_delegate_chromeos.h" |
| #endif |
| |
| using base::TimeDelta; |
| using base::TimeTicks; |
| using content::BrowserThread; |
| using content::WebContents; |
| |
| namespace resource_coordinator { |
| namespace { |
| |
| using LoadingState = TabLoadTracker::LoadingState; |
| |
| // The default timeout time after which the next background tab gets loaded if |
| // the previous tab has not finished loading yet. This is ignored in kPaused |
| // loading mode. |
| constexpr TimeDelta kDefaultBackgroundTabLoadTimeout = |
| TimeDelta::FromSeconds(10); |
| |
| // The number of loading slots for background tabs. TabManager will start to |
| // load the next background tab when the loading slots free up. |
| constexpr size_t kNumOfLoadingSlots = 1; |
| |
| // The default interval in seconds after which to adjust the oom_score_adj |
| // value. |
| constexpr int kAdjustmentIntervalSeconds = 10; |
| |
| struct LifecycleUnitAndSortKey { |
| explicit LifecycleUnitAndSortKey(LifecycleUnit* lifecycle_unit) |
| : lifecycle_unit(lifecycle_unit), |
| sort_key(lifecycle_unit->GetSortKey()) {} |
| |
| bool operator<(const LifecycleUnitAndSortKey& other) const { |
| return sort_key < other.sort_key; |
| } |
| bool operator>(const LifecycleUnitAndSortKey& other) const { |
| return sort_key > other.sort_key; |
| } |
| |
| LifecycleUnit* lifecycle_unit; |
| LifecycleUnit::SortKey sort_key; |
| }; |
| |
| std::unique_ptr<base::trace_event::ConvertableToTraceFormat> DataAsTraceValue( |
| TabManager::BackgroundTabLoadingMode mode, |
| size_t num_of_pending_navigations, |
| size_t num_of_loading_contents) { |
| std::unique_ptr<base::trace_event::TracedValue> data( |
| new base::trace_event::TracedValue()); |
| data->SetInteger("background_tab_loading_mode", mode); |
| data->SetInteger("num_of_pending_navigations", num_of_pending_navigations); |
| data->SetInteger("num_of_loading_contents", num_of_loading_contents); |
| return std::move(data); |
| } |
| |
| int GetNumLoadedLifecycleUnits(LifecycleUnitSet lifecycle_unit_set) { |
| int num_loaded_lifecycle_units = 0; |
| for (auto* lifecycle_unit : lifecycle_unit_set) { |
| LifecycleUnitState state = lifecycle_unit->GetState(); |
| if (state != LifecycleUnitState::DISCARDED && |
| state != LifecycleUnitState::PENDING_DISCARD) { |
| num_loaded_lifecycle_units++; |
| } |
| } |
| return num_loaded_lifecycle_units; |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // TabManager |
| |
| class TabManager::TabManagerSessionRestoreObserver final |
| : public SessionRestoreObserver { |
| public: |
| explicit TabManagerSessionRestoreObserver(TabManager* tab_manager) |
| : tab_manager_(tab_manager) { |
| SessionRestore::AddObserver(this); |
| } |
| |
| ~TabManagerSessionRestoreObserver() { SessionRestore::RemoveObserver(this); } |
| |
| // SessionRestoreObserver implementation: |
| void OnSessionRestoreStartedLoadingTabs() override { |
| tab_manager_->OnSessionRestoreStartedLoadingTabs(); |
| } |
| |
| void OnSessionRestoreFinishedLoadingTabs() override { |
| tab_manager_->OnSessionRestoreFinishedLoadingTabs(); |
| } |
| |
| void OnWillRestoreTab(WebContents* web_contents) override { |
| tab_manager_->OnWillRestoreTab(web_contents); |
| } |
| |
| private: |
| TabManager* tab_manager_; |
| }; |
| |
| constexpr base::TimeDelta TabManager::kDefaultMinTimeToPurge; |
| |
| TabManager::TabManager(PageSignalReceiver* page_signal_receiver, |
| TabLoadTracker* tab_load_tracker) |
| : state_transitions_callback_( |
| base::BindRepeating(&TabManager::PerformStateTransitions, |
| base::Unretained(this))), |
| browser_tab_strip_tracker_(this, nullptr, nullptr), |
| is_session_restore_loading_tabs_(false), |
| restored_tab_count_(0u), |
| background_tab_loading_mode_(BackgroundTabLoadingMode::kStaggered), |
| loading_slots_(kNumOfLoadingSlots), |
| tab_load_tracker_(tab_load_tracker), |
| weak_ptr_factory_(this) { |
| #if defined(OS_CHROMEOS) |
| delegate_.reset(new TabManagerDelegate(weak_ptr_factory_.GetWeakPtr())); |
| #endif |
| browser_tab_strip_tracker_.Init(); |
| session_restore_observer_.reset(new TabManagerSessionRestoreObserver(this)); |
| if (PageSignalReceiver::IsEnabled()) { |
| resource_coordinator_signal_observer_.reset( |
| new ResourceCoordinatorSignalObserver(page_signal_receiver)); |
| } |
| stats_collector_.reset(new TabManagerStatsCollector()); |
| proactive_freeze_discard_params_ = |
| GetStaticProactiveTabFreezeAndDiscardParams(); |
| tab_load_tracker_->AddObserver(this); |
| intervention_policy_database_.reset(new InterventionPolicyDatabase()); |
| |
| // TabManager works in the absence of DesktopSessionDurationTracker for tests. |
| if (metrics::DesktopSessionDurationTracker::IsInitialized()) |
| metrics::DesktopSessionDurationTracker::Get()->AddObserver(this); |
| } |
| |
| TabManager::~TabManager() { |
| tab_load_tracker_->RemoveObserver(this); |
| resource_coordinator_signal_observer_.reset(); |
| Stop(); |
| |
| if (metrics::DesktopSessionDurationTracker::IsInitialized()) |
| metrics::DesktopSessionDurationTracker::Get()->RemoveObserver(this); |
| } |
| |
| void TabManager::Start() { |
| background_tab_loading_mode_ = BackgroundTabLoadingMode::kStaggered; |
| |
| #if defined(OS_WIN) || defined(OS_MACOSX) |
| // Note that discarding is now enabled by default. This check is kept as a |
| // kill switch. |
| // TODO(georgesak): remote this when deemed not needed anymore. |
| if (!base::FeatureList::IsEnabled(features::kAutomaticTabDiscarding)) |
| return; |
| #endif |
| |
| if (!update_timer_.IsRunning()) { |
| update_timer_.Start(FROM_HERE, |
| TimeDelta::FromSeconds(kAdjustmentIntervalSeconds), |
| this, &TabManager::UpdateTimerCallback); |
| } |
| |
| // MemoryPressureMonitor is not implemented on Linux so far and tabs are never |
| // discarded. |
| #if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_CHROMEOS) |
| // Create a |MemoryPressureListener| to listen for memory events when |
| // MemoryCoordinator is disabled. When MemoryCoordinator is enabled |
| // it asks TabManager to do tab discarding. |
| base::MemoryPressureMonitor* monitor = base::MemoryPressureMonitor::Get(); |
| if (monitor && !base::FeatureList::IsEnabled(features::kMemoryCoordinator)) { |
| memory_pressure_listener_.reset(new base::MemoryPressureListener( |
| base::Bind(&TabManager::OnMemoryPressure, base::Unretained(this)))); |
| base::MemoryPressureListener::MemoryPressureLevel level = |
| monitor->GetCurrentPressureLevel(); |
| if (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) { |
| OnMemoryPressure(level); |
| } |
| } |
| #endif |
| // purge-and-suspend param is used for Purge+Suspend finch experiment |
| // in the following way: |
| // https://docs.google.com/document/d/1hPHkKtXXBTlsZx9s-9U17XC-ofEIzPo9FYbBEc7PPbk/edit?usp=sharing |
| std::string purge_and_suspend_time = variations::GetVariationParamValue( |
| "PurgeAndSuspendAggressive", "purge-and-suspend-time"); |
| unsigned int min_time_to_purge_sec = 0; |
| if (purge_and_suspend_time.empty() || |
| !base::StringToUint(purge_and_suspend_time, &min_time_to_purge_sec)) |
| min_time_to_purge_ = kDefaultMinTimeToPurge; |
| else |
| min_time_to_purge_ = base::TimeDelta::FromSeconds(min_time_to_purge_sec); |
| |
| std::string max_purge_and_suspend_time = variations::GetVariationParamValue( |
| "PurgeAndSuspendAggressive", "max-purge-and-suspend-time"); |
| unsigned int max_time_to_purge_sec = 0; |
| // If max-purge-and-suspend-time is not specified or |
| // max-purge-and-suspend-time is not valid (not number or smaller than |
| // min-purge-and-suspend-time), use default max-time-to-purge, i.e. |
| // min-time-to-purge times kDefaultMinMaxTimeToPurgeRatio. |
| if (max_purge_and_suspend_time.empty() || |
| !base::StringToUint(max_purge_and_suspend_time, &max_time_to_purge_sec) || |
| max_time_to_purge_sec < min_time_to_purge_.InSeconds()) |
| max_time_to_purge_ = min_time_to_purge_ * kDefaultMinMaxTimeToPurgeRatio; |
| else |
| max_time_to_purge_ = base::TimeDelta::FromSeconds(max_time_to_purge_sec); |
| } |
| |
| void TabManager::Stop() { |
| update_timer_.Stop(); |
| force_load_timer_.reset(); |
| memory_pressure_listener_.reset(); |
| } |
| |
| LifecycleUnitVector TabManager::GetSortedLifecycleUnits() { |
| std::vector<LifecycleUnitAndSortKey> lifecycle_units_and_sort_keys; |
| lifecycle_units_and_sort_keys.reserve(lifecycle_units_.size()); |
| for (auto* lifecycle_unit : lifecycle_units_) |
| lifecycle_units_and_sort_keys.emplace_back(lifecycle_unit); |
| |
| std::sort(lifecycle_units_and_sort_keys.begin(), |
| lifecycle_units_and_sort_keys.end()); |
| |
| LifecycleUnitVector sorted_lifecycle_units; |
| sorted_lifecycle_units.reserve(lifecycle_units_and_sort_keys.size()); |
| for (auto& lifecycle_unit_and_sort_key : lifecycle_units_and_sort_keys) { |
| sorted_lifecycle_units.push_back( |
| lifecycle_unit_and_sort_key.lifecycle_unit); |
| } |
| |
| return sorted_lifecycle_units; |
| } |
| |
| void TabManager::DiscardTab(LifecycleUnitDiscardReason reason) { |
| if (reason == LifecycleUnitDiscardReason::URGENT) { |
| stats_collector_->RecordWillDiscardUrgently(GetNumAliveTabs()); |
| resource_coordinator::TabActivityWatcher::GetInstance() |
| ->LogOldestNTabFeatures(); |
| } |
| |
| #if defined(OS_CHROMEOS) |
| // Call Chrome OS specific low memory handling process. |
| delegate_->LowMemoryKill(reason); |
| #else |
| DiscardTabImpl(reason); |
| #endif // defined(OS_CHROMEOS) |
| } |
| |
| WebContents* TabManager::DiscardTabByExtension(content::WebContents* contents) { |
| if (contents) { |
| TabLifecycleUnitExternal* tab_lifecycle_unit_external = |
| TabLifecycleUnitExternal::FromWebContents(contents); |
| DCHECK(tab_lifecycle_unit_external); |
| if (tab_lifecycle_unit_external->DiscardTab()) |
| return tab_lifecycle_unit_external->GetWebContents(); |
| return nullptr; |
| } |
| |
| return DiscardTabImpl(LifecycleUnitDiscardReason::EXTERNAL); |
| } |
| |
| void TabManager::LogMemoryAndDiscardTab(LifecycleUnitDiscardReason reason) { |
| // Discard immediately without waiting for LogMemory() (https://crbug/850545). |
| // Consider removing LogMemory() at all if nobody cares about the log. |
| LogMemory("Tab Discards Memory details"); |
| PurgeMemoryAndDiscardTab(reason); |
| } |
| |
| void TabManager::LogMemory(const std::string& title) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| memory::OomMemoryDetails::Log(title); |
| } |
| |
| void TabManager::AddObserver(TabLifecycleObserver* observer) { |
| TabLifecycleUnitExternal::AddTabLifecycleObserver(observer); |
| } |
| |
| void TabManager::RemoveObserver(TabLifecycleObserver* observer) { |
| TabLifecycleUnitExternal::RemoveTabLifecycleObserver(observer); |
| } |
| |
| bool TabManager::CanPurgeBackgroundedRenderer(int render_process_id) const { |
| for (LifecycleUnit* lifecycle_unit : lifecycle_units_) { |
| TabLifecycleUnitExternal* tab_lifecycle_unit_external = |
| lifecycle_unit->AsTabLifecycleUnitExternal(); |
| // For now, all LifecycleUnits are TabLifecycleUnitExternals. |
| DCHECK(tab_lifecycle_unit_external); |
| content::WebContents* content = |
| tab_lifecycle_unit_external->GetWebContents(); |
| DCHECK(content); |
| |
| if (content->IsCrashed()) |
| continue; |
| if (content->GetMainFrame()->GetProcess()->GetID() != render_process_id) |
| continue; |
| if (!lifecycle_unit->CanPurge()) |
| return false; |
| } |
| return true; |
| } |
| |
| size_t TabManager::GetBackgroundTabLoadingCount() const { |
| if (!IsInBackgroundTabOpeningSession()) |
| return 0; |
| |
| return loading_contents_.size(); |
| } |
| |
| size_t TabManager::GetBackgroundTabPendingCount() const { |
| if (!IsInBackgroundTabOpeningSession()) |
| return 0; |
| |
| return pending_navigations_.size(); |
| } |
| |
| int TabManager::GetTabCount() const { |
| int tab_count = 0; |
| for (auto* browser : *BrowserList::GetInstance()) |
| tab_count += browser->tab_strip_model()->count(); |
| return tab_count; |
| } |
| |
| // static |
| bool TabManager::IsTabInSessionRestore(WebContents* web_contents) { |
| return GetWebContentsData(web_contents)->is_in_session_restore(); |
| } |
| |
| // static |
| bool TabManager::IsTabRestoredInForeground(WebContents* web_contents) { |
| return GetWebContentsData(web_contents)->is_restored_in_foreground(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabManager, private: |
| |
| // static |
| void TabManager::PurgeMemoryAndDiscardTab(LifecycleUnitDiscardReason reason) { |
| TabManager* manager = g_browser_process->GetTabManager(); |
| manager->PurgeBrowserMemory(); |
| manager->DiscardTab(reason); |
| } |
| |
| // static |
| bool TabManager::IsInternalPage(const GURL& url) { |
| // There are many chrome:// UI URLs, but only look for the ones that users |
| // are likely to have open. Most of the benefit is the from NTP URL. |
| const char* const kInternalPagePrefixes[] = { |
| chrome::kChromeUIDownloadsURL, chrome::kChromeUIHistoryURL, |
| chrome::kChromeUINewTabURL, chrome::kChromeUISettingsURL}; |
| // Prefix-match against the table above. Use strncmp to avoid allocating |
| // memory to convert the URL prefix constants into std::strings. |
| for (size_t i = 0; i < arraysize(kInternalPagePrefixes); ++i) { |
| if (!strncmp(url.spec().c_str(), kInternalPagePrefixes[i], |
| strlen(kInternalPagePrefixes[i]))) |
| return true; |
| } |
| return false; |
| } |
| |
| void TabManager::PurgeBrowserMemory() { |
| // Based on experimental evidence, attempts to free memory from renderers |
| // have been too slow to use in OOM situations (V8 garbage collection) or |
| // do not lead to persistent decreased usage (image/bitmap caches). This |
| // function therefore only targets large blocks of memory in the browser. |
| // Note that other objects will listen to MemoryPressureListener events |
| // to release memory. |
| for (auto* web_contents : AllTabContentses()) { |
| // Screenshots can consume ~5 MB per web contents for platforms that do |
| // touch back/forward. |
| web_contents->GetController().ClearAllScreenshots(); |
| } |
| } |
| |
| // This function is called when |update_timer_| fires. It will adjust the clock |
| // if needed (if it detects that the machine was asleep) and will fire the stats |
| // updating on ChromeOS via the delegate. This function also tries to purge |
| // cache memory. |
| void TabManager::UpdateTimerCallback() { |
| // If Chrome is shutting down, do not do anything. |
| if (g_browser_process->IsShuttingDown()) |
| return; |
| |
| if (BrowserList::GetInstance()->empty()) |
| return; |
| |
| #if defined(OS_CHROMEOS) |
| // This starts the CrOS specific OOM adjustments in /proc/<pid>/oom_score_adj. |
| delegate_->AdjustOomPriorities(); |
| #endif |
| |
| PurgeBackgroundedTabsIfNeeded(); |
| } |
| |
| base::TimeDelta TabManager::GetTimeToPurge( |
| base::TimeDelta min_time_to_purge, |
| base::TimeDelta max_time_to_purge) const { |
| return base::TimeDelta::FromSeconds(base::RandInt( |
| min_time_to_purge.InSeconds(), max_time_to_purge.InSeconds())); |
| } |
| |
| bool TabManager::ShouldPurgeNow(content::WebContents* content) const { |
| if (GetWebContentsData(content)->is_purged()) |
| return false; |
| if (TabLifecycleUnitExternal::FromWebContents(content)->IsDiscarded()) |
| return false; |
| |
| base::TimeDelta time_passed = |
| NowTicks() - GetWebContentsData(content)->LastInactiveTime(); |
| return time_passed > GetWebContentsData(content)->time_to_purge(); |
| } |
| |
| void TabManager::PurgeBackgroundedTabsIfNeeded() { |
| for (LifecycleUnit* lifecycle_unit : lifecycle_units_) { |
| TabLifecycleUnitExternal* tab_lifecycle_unit_external = |
| lifecycle_unit->AsTabLifecycleUnitExternal(); |
| // For now, all LifecycleUnits are TabLifecycleUnitExternals. |
| DCHECK(tab_lifecycle_unit_external); |
| content::WebContents* content = |
| tab_lifecycle_unit_external->GetWebContents(); |
| DCHECK(content); |
| |
| if (content->IsCrashed()) |
| continue; |
| |
| content::RenderProcessHost* render_process_host = |
| content->GetMainFrame()->GetProcess(); |
| int render_process_id = render_process_host->GetID(); |
| |
| if (!render_process_host->IsProcessBackgrounded()) |
| continue; |
| if (!CanPurgeBackgroundedRenderer(render_process_id)) |
| continue; |
| |
| bool purge_now = ShouldPurgeNow(content); |
| if (!purge_now) |
| continue; |
| |
| // Since |content|'s tab is kept inactive and background for more than |
| // time-to-purge time, its purged state changes: false => true. |
| GetWebContentsData(content)->set_is_purged(true); |
| // TODO(tasak): rename PurgeAndSuspend with a better name, e.g. |
| // RequestPurgeCache, because we don't suspend any renderers. |
| render_process_host->PurgeAndSuspend(); |
| } |
| } |
| |
| void TabManager::PauseBackgroundTabOpeningIfNeeded() { |
| TRACE_EVENT_INSTANT0("navigation", |
| "TabManager::PauseBackgroundTabOpeningIfNeeded", |
| TRACE_EVENT_SCOPE_THREAD); |
| if (IsInBackgroundTabOpeningSession()) { |
| stats_collector_->TrackPausedBackgroundTabs(pending_navigations_.size()); |
| stats_collector_->OnBackgroundTabOpeningSessionEnded(); |
| } |
| |
| background_tab_loading_mode_ = BackgroundTabLoadingMode::kPaused; |
| } |
| |
| void TabManager::ResumeBackgroundTabOpeningIfNeeded() { |
| TRACE_EVENT_INSTANT0("navigation", |
| "TabManager::ResumeBackgroundTabOpeningIfNeeded", |
| TRACE_EVENT_SCOPE_THREAD); |
| background_tab_loading_mode_ = BackgroundTabLoadingMode::kStaggered; |
| LoadNextBackgroundTabIfNeeded(); |
| |
| if (IsInBackgroundTabOpeningSession()) |
| stats_collector_->OnBackgroundTabOpeningSessionStarted(); |
| } |
| |
| void TabManager::OnMemoryPressure( |
| base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) { |
| // If Chrome is shutting down, do not do anything. |
| if (g_browser_process->IsShuttingDown()) |
| return; |
| |
| // TODO(crbug.com/762775): Pause or resume background tab opening based on |
| // memory pressure signal after it becomes more reliable. |
| switch (memory_pressure_level) { |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE: |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE: |
| return; |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL: |
| LogMemoryAndDiscardTab(LifecycleUnitDiscardReason::URGENT); |
| return; |
| } |
| NOTREACHED(); |
| // TODO(skuhne): If more memory pressure levels are introduced, consider |
| // calling PurgeBrowserMemory() before CRITICAL is reached. |
| } |
| |
| void TabManager::OnActiveTabChanged(content::WebContents* old_contents, |
| content::WebContents* new_contents) { |
| // An active tab is not purged. |
| // Calling GetWebContentsData() early ensures that the WebContentsData is |
| // created for |new_contents|, which |stats_collector_| expects. |
| GetWebContentsData(new_contents)->set_is_purged(false); |
| |
| // If |old_contents| is set, that tab has switched from being active to |
| // inactive, so record the time of that transition. |
| if (old_contents) { |
| GetWebContentsData(old_contents)->SetLastInactiveTime(NowTicks()); |
| // Re-setting time-to-purge every time a tab becomes inactive. |
| GetWebContentsData(old_contents) |
| ->set_time_to_purge( |
| GetTimeToPurge(min_time_to_purge_, max_time_to_purge_)); |
| // Only record switch-to-tab metrics when a switch happens, i.e. |
| // |old_contents| is set. |
| stats_collector_->RecordSwitchToTab(old_contents, new_contents); |
| } |
| |
| ResumeTabNavigationIfNeeded(new_contents); |
| } |
| |
| void TabManager::OnTabInserted(content::WebContents* contents, |
| bool foreground) { |
| // Only interested in background tabs, as foreground tabs get taken care of by |
| // OnActiveTabChanged. |
| if (foreground) |
| return; |
| |
| // A new background tab is similar to having a tab switch from being active to |
| // inactive. |
| GetWebContentsData(contents)->SetLastInactiveTime(NowTicks()); |
| // Re-setting time-to-purge every time a tab becomes inactive. |
| GetWebContentsData(contents)->set_time_to_purge( |
| GetTimeToPurge(min_time_to_purge_, max_time_to_purge_)); |
| } |
| |
| void TabManager::OnTabStripModelChanged( |
| TabStripModel* tab_strip_model, |
| const TabStripModelChange& change, |
| const TabStripSelectionChange& selection) { |
| if (change.type() == TabStripModelChange::kInserted) { |
| for (const auto& delta : change.deltas()) { |
| OnTabInserted(delta.insert.contents, |
| delta.insert.contents == selection.new_contents); |
| } |
| } else if (change.type() == TabStripModelChange::kReplaced) { |
| for (const auto& delta : change.deltas()) { |
| WebContentsData::CopyState(delta.replace.old_contents, |
| delta.replace.new_contents); |
| } |
| } |
| |
| if (selection.active_tab_changed() && !tab_strip_model->empty()) |
| OnActiveTabChanged(selection.old_contents, selection.new_contents); |
| } |
| |
| void TabManager::OnStartTracking(content::WebContents* web_contents, |
| LoadingState loading_state) { |
| GetWebContentsData(web_contents)->SetTabLoadingState(loading_state); |
| } |
| |
| void TabManager::OnLoadingStateChange(content::WebContents* web_contents, |
| LoadingState old_loading_state, |
| LoadingState new_loading_state) { |
| GetWebContentsData(web_contents)->SetTabLoadingState(new_loading_state); |
| |
| if (new_loading_state == LoadingState::LOADED) { |
| bool was_in_background_tab_opening_session = |
| IsInBackgroundTabOpeningSession(); |
| |
| loading_contents_.erase(web_contents); |
| stats_collector_->OnTabIsLoaded(web_contents); |
| LoadNextBackgroundTabIfNeeded(); |
| |
| if (was_in_background_tab_opening_session && |
| !IsInBackgroundTabOpeningSession()) { |
| stats_collector_->OnBackgroundTabOpeningSessionEnded(); |
| } |
| |
| // Once a tab is loaded, it might be eligible for freezing. |
| SchedulePerformStateTransitions(base::TimeDelta()); |
| } |
| } |
| |
| void TabManager::OnStopTracking(content::WebContents* web_contents, |
| LoadingState loading_state) { |
| GetWebContentsData(web_contents)->SetTabLoadingState(loading_state); |
| } |
| |
| void TabManager::OnSessionStarted(base::TimeTicks session_start) { |
| // LifecycleUnits might become eligible for proactive discarding when Chrome |
| // starts being used. |
| SchedulePerformStateTransitions(base::TimeDelta()); |
| } |
| |
| // static |
| TabManager::WebContentsData* TabManager::GetWebContentsData( |
| content::WebContents* contents) { |
| WebContentsData::CreateForWebContents(contents); |
| return WebContentsData::FromWebContents(contents); |
| } |
| |
| // TODO(jamescook): This should consider tabs with references to other tabs, |
| // such as tabs created with JavaScript window.open(). Potentially consider |
| // discarding the entire set together, or use that in the priority computation. |
| content::WebContents* TabManager::DiscardTabImpl( |
| LifecycleUnitDiscardReason reason) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| for (LifecycleUnit* lifecycle_unit : GetSortedLifecycleUnits()) { |
| DecisionDetails decision_details; |
| if (lifecycle_unit->CanDiscard(reason, &decision_details) && |
| lifecycle_unit->Discard(reason)) { |
| TabLifecycleUnitExternal* tab_lifecycle_unit_external = |
| lifecycle_unit->AsTabLifecycleUnitExternal(); |
| // For now, all LifecycleUnits are TabLifecycleUnitExternals. |
| DCHECK(tab_lifecycle_unit_external); |
| |
| return tab_lifecycle_unit_external->GetWebContents(); |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| void TabManager::OnSessionRestoreStartedLoadingTabs() { |
| DCHECK(!is_session_restore_loading_tabs_); |
| is_session_restore_loading_tabs_ = true; |
| } |
| |
| void TabManager::OnSessionRestoreFinishedLoadingTabs() { |
| DCHECK(is_session_restore_loading_tabs_); |
| is_session_restore_loading_tabs_ = false; |
| restored_tab_count_ = 0u; |
| } |
| |
| void TabManager::OnWillRestoreTab(WebContents* contents) { |
| WebContentsData* data = GetWebContentsData(contents); |
| DCHECK(!data->is_in_session_restore()); |
| data->SetIsInSessionRestore(true); |
| data->SetIsRestoredInForeground(contents->GetVisibility() != |
| content::Visibility::HIDDEN); |
| restored_tab_count_++; |
| |
| // TabUIHelper is initialized in TabHelpers::AttachTabHelpers. But this place |
| // gets called earlier than that. So for restored tabs, also initialize their |
| // TabUIHelper here. |
| TabUIHelper::CreateForWebContents(contents); |
| TabUIHelper::FromWebContents(contents)->set_created_by_session_restore(true); |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| TabManager::MaybeThrottleNavigation(BackgroundTabNavigationThrottle* throttle) { |
| content::WebContents* contents = |
| throttle->navigation_handle()->GetWebContents(); |
| DCHECK_EQ(contents->GetVisibility(), content::Visibility::HIDDEN); |
| |
| // Skip delaying the navigation if this tab is in session restore, whose |
| // loading is already controlled by TabLoader. |
| if (GetWebContentsData(contents)->is_in_session_restore()) |
| return content::NavigationThrottle::PROCEED; |
| |
| if (background_tab_loading_mode_ == BackgroundTabLoadingMode::kStaggered && |
| !IsInBackgroundTabOpeningSession()) { |
| stats_collector_->OnBackgroundTabOpeningSessionStarted(); |
| } |
| |
| stats_collector_->TrackNewBackgroundTab(pending_navigations_.size(), |
| loading_contents_.size()); |
| |
| if (!base::FeatureList::IsEnabled( |
| features::kStaggeredBackgroundTabOpeningExperiment) || |
| CanLoadNextTab()) { |
| loading_contents_.insert(contents); |
| stats_collector_->TrackBackgroundTabLoadAutoStarted(); |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| // Notify TabUIHelper that the navigation is delayed, so that the tab UI such |
| // as favicon and title can be updated accordingly. |
| TabUIHelper::FromWebContents(contents)->NotifyInitialNavigationDelayed(true); |
| pending_navigations_.push_back(throttle); |
| std::stable_sort(pending_navigations_.begin(), pending_navigations_.end(), |
| ComparePendingNavigations); |
| |
| TRACE_EVENT_INSTANT1( |
| "navigation", "TabManager::MaybeThrottleNavigation", |
| TRACE_EVENT_SCOPE_THREAD, "data", |
| DataAsTraceValue(background_tab_loading_mode_, |
| pending_navigations_.size(), loading_contents_.size())); |
| |
| StartForceLoadTimer(); |
| return content::NavigationThrottle::DEFER; |
| } |
| |
| bool TabManager::IsInBackgroundTabOpeningSession() const { |
| if (background_tab_loading_mode_ != BackgroundTabLoadingMode::kStaggered) |
| return false; |
| |
| return !(pending_navigations_.empty() && loading_contents_.empty()); |
| } |
| |
| bool TabManager::CanLoadNextTab() const { |
| if (background_tab_loading_mode_ != BackgroundTabLoadingMode::kStaggered) |
| return false; |
| |
| // TabManager can only load the next tab when the loading slots free up. The |
| // loading slot limit can be exceeded when |force_load_timer_| fires or when |
| // the user selects a background tab. |
| if (loading_contents_.size() < loading_slots_) |
| return true; |
| |
| return false; |
| } |
| |
| void TabManager::OnDidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| auto it = pending_navigations_.begin(); |
| while (it != pending_navigations_.end()) { |
| BackgroundTabNavigationThrottle* throttle = *it; |
| if (throttle->navigation_handle() == navigation_handle) { |
| TRACE_EVENT_INSTANT1("navigation", "TabManager::OnDidFinishNavigation", |
| TRACE_EVENT_SCOPE_THREAD, |
| "found_navigation_handle_to_remove", true); |
| pending_navigations_.erase(it); |
| break; |
| } |
| it++; |
| } |
| } |
| |
| void TabManager::OnWebContentsDestroyed(content::WebContents* contents) { |
| bool was_in_background_tab_opening_session = |
| IsInBackgroundTabOpeningSession(); |
| |
| RemovePendingNavigationIfNeeded(contents); |
| loading_contents_.erase(contents); |
| stats_collector_->OnWebContentsDestroyed(contents); |
| LoadNextBackgroundTabIfNeeded(); |
| |
| if (was_in_background_tab_opening_session && |
| !IsInBackgroundTabOpeningSession()) { |
| stats_collector_->OnBackgroundTabOpeningSessionEnded(); |
| } |
| } |
| |
| void TabManager::StartForceLoadTimer() { |
| TRACE_EVENT_INSTANT1( |
| "navigation", "TabManager::StartForceLoadTimer", TRACE_EVENT_SCOPE_THREAD, |
| "data", |
| DataAsTraceValue(background_tab_loading_mode_, |
| pending_navigations_.size(), loading_contents_.size())); |
| |
| if (force_load_timer_) |
| force_load_timer_->Stop(); |
| else |
| force_load_timer_ = std::make_unique<base::OneShotTimer>(GetTickClock()); |
| |
| force_load_timer_->Start(FROM_HERE, |
| GetTabLoadTimeout(kDefaultBackgroundTabLoadTimeout), |
| this, &TabManager::LoadNextBackgroundTabIfNeeded); |
| } |
| |
| void TabManager::LoadNextBackgroundTabIfNeeded() { |
| TRACE_EVENT_INSTANT2( |
| "navigation", "TabManager::LoadNextBackgroundTabIfNeeded", |
| TRACE_EVENT_SCOPE_THREAD, "is_force_load_timer_running", |
| IsForceLoadTimerRunning(), "data", |
| DataAsTraceValue(background_tab_loading_mode_, |
| pending_navigations_.size(), loading_contents_.size())); |
| |
| if (background_tab_loading_mode_ != BackgroundTabLoadingMode::kStaggered) |
| return; |
| |
| // Do not load more background tabs until TabManager can load the next tab. |
| // Ignore this constraint if the timer fires to force loading the next |
| // background tab. |
| if (IsForceLoadTimerRunning() && !CanLoadNextTab()) |
| return; |
| |
| if (pending_navigations_.empty()) |
| return; |
| |
| stats_collector_->OnWillLoadNextBackgroundTab(!IsForceLoadTimerRunning()); |
| BackgroundTabNavigationThrottle* throttle = pending_navigations_.front(); |
| pending_navigations_.erase(pending_navigations_.begin()); |
| ResumeNavigation(throttle); |
| stats_collector_->TrackBackgroundTabLoadAutoStarted(); |
| |
| StartForceLoadTimer(); |
| } |
| |
| void TabManager::ResumeTabNavigationIfNeeded(content::WebContents* contents) { |
| BackgroundTabNavigationThrottle* throttle = |
| RemovePendingNavigationIfNeeded(contents); |
| if (throttle) { |
| ResumeNavigation(throttle); |
| stats_collector_->TrackBackgroundTabLoadUserInitiated(); |
| } |
| } |
| |
| void TabManager::ResumeNavigation(BackgroundTabNavigationThrottle* throttle) { |
| content::WebContents* contents = |
| throttle->navigation_handle()->GetWebContents(); |
| loading_contents_.insert(contents); |
| TabUIHelper::FromWebContents(contents)->NotifyInitialNavigationDelayed(false); |
| |
| throttle->ResumeNavigation(); |
| } |
| |
| BackgroundTabNavigationThrottle* TabManager::RemovePendingNavigationIfNeeded( |
| content::WebContents* contents) { |
| auto it = pending_navigations_.begin(); |
| while (it != pending_navigations_.end()) { |
| BackgroundTabNavigationThrottle* throttle = *it; |
| if (throttle->navigation_handle()->GetWebContents() == contents) { |
| pending_navigations_.erase(it); |
| return throttle; |
| } |
| it++; |
| } |
| return nullptr; |
| } |
| |
| // static |
| bool TabManager::ComparePendingNavigations( |
| const BackgroundTabNavigationThrottle* first, |
| const BackgroundTabNavigationThrottle* second) { |
| bool first_is_internal_page = |
| IsInternalPage(first->navigation_handle()->GetURL()); |
| bool second_is_internal_page = |
| IsInternalPage(second->navigation_handle()->GetURL()); |
| |
| if (first_is_internal_page != second_is_internal_page) |
| return !first_is_internal_page; |
| |
| return false; |
| } |
| |
| int TabManager::GetNumAliveTabs() const { |
| int tab_count = 0; |
| for (auto* browser : *BrowserList::GetInstance()) { |
| TabStripModel* tab_strip_model = browser->tab_strip_model(); |
| for (int index = 0; index < tab_strip_model->count(); ++index) { |
| content::WebContents* contents = tab_strip_model->GetWebContentsAt(index); |
| if (!TabLifecycleUnitExternal::FromWebContents(contents)->IsDiscarded()) |
| ++tab_count; |
| } |
| } |
| |
| tab_count -= pending_navigations_.size(); |
| DCHECK_GE(tab_count, 0); |
| |
| return tab_count; |
| } |
| |
| bool TabManager::IsTabLoadingForTest(content::WebContents* contents) const { |
| if (base::ContainsKey(loading_contents_, contents)) |
| return true; |
| DCHECK_NE(LoadingState::LOADING, |
| GetWebContentsData(contents)->tab_loading_state()); |
| return false; |
| } |
| |
| bool TabManager::IsNavigationDelayedForTest( |
| const content::NavigationHandle* navigation_handle) const { |
| for (const auto* it : pending_navigations_) { |
| if (it->navigation_handle() == navigation_handle) |
| return true; |
| } |
| return false; |
| } |
| |
| bool TabManager::IsForceLoadTimerRunning() const { |
| return force_load_timer_ && force_load_timer_->IsRunning(); |
| } |
| |
| base::TimeDelta TabManager::GetTimeInBackgroundBeforeProactiveDiscard() const { |
| // Exceed high threshold - in excessive state. |
| if (num_loaded_lifecycle_units_ >= |
| proactive_freeze_discard_params_.high_loaded_tab_count) { |
| return base::TimeDelta(); |
| } |
| |
| // Exceed moderate threshold - in high state. |
| if (num_loaded_lifecycle_units_ >= |
| proactive_freeze_discard_params_.moderate_loaded_tab_count) { |
| return proactive_freeze_discard_params_.high_occluded_timeout; |
| } |
| |
| // Exceed low threshold - in moderate state. |
| if (num_loaded_lifecycle_units_ >= |
| proactive_freeze_discard_params_.low_loaded_tab_count) { |
| return proactive_freeze_discard_params_.moderate_occluded_timeout; |
| } |
| |
| // Didn't meet any thresholds - in low state. |
| return proactive_freeze_discard_params_.low_occluded_timeout; |
| } |
| |
| void TabManager::SchedulePerformStateTransitions(base::TimeDelta delay) { |
| if (!state_transitions_timer_) { |
| state_transitions_timer_ = |
| std::make_unique<base::OneShotTimer>(GetTickClock()); |
| } |
| |
| state_transitions_timer_->Start(FROM_HERE, delay, |
| state_transitions_callback_); |
| } |
| |
| void TabManager::PerformStateTransitions() { |
| if (!base::FeatureList::IsEnabled(features::kProactiveTabFreezeAndDiscard)) |
| return; |
| |
| base::TimeTicks next_state_transition_time = base::TimeTicks::Max(); |
| const base::TimeTicks now = NowTicks(); |
| LifecycleUnit* oldest_discardable_lifecycle_unit = nullptr; |
| LifecycleUnit* oldest_frozen_lifecycle_unit = nullptr; |
| |
| for (LifecycleUnit* lifecycle_unit : lifecycle_units_) { |
| // Maybe freeze the LifecycleUnit. |
| next_state_transition_time = |
| std::min(MaybeFreezeLifecycleUnit(lifecycle_unit, now), |
| next_state_transition_time); |
| |
| // Keep track of the discardable LifecycleUnit that has been hidden for the |
| // longest time. It might be discarded below. |
| DecisionDetails discard_details; |
| if (lifecycle_unit->CanDiscard(LifecycleUnitDiscardReason::PROACTIVE, |
| &discard_details)) { |
| if (!oldest_discardable_lifecycle_unit || |
| lifecycle_unit->GetChromeUsageTimeWhenHidden() < |
| oldest_discardable_lifecycle_unit |
| ->GetChromeUsageTimeWhenHidden()) { |
| oldest_discardable_lifecycle_unit = lifecycle_unit; |
| } |
| } |
| |
| // Keep track of the LifecycleUnit that has been frozen for the longest |
| // time. It might be unfrozen below. |
| if (lifecycle_unit->GetState() == LifecycleUnitState::FROZEN && |
| (!oldest_frozen_lifecycle_unit || |
| lifecycle_unit->GetWallTimeWhenHidden() < |
| oldest_frozen_lifecycle_unit->GetWallTimeWhenHidden())) { |
| oldest_frozen_lifecycle_unit = lifecycle_unit; |
| } |
| } |
| |
| // Unfreeze the LifecycleUnit that has been frozen for the longest time if it |
| // has been frozen long enough and a sufficient amount of time elapsed since |
| // the last unfreeze. |
| if (proactive_freeze_discard_params_.should_periodically_unfreeze && |
| oldest_frozen_lifecycle_unit) { |
| next_state_transition_time = |
| std::min(MaybeUnfreezeLifecycleUnit(oldest_frozen_lifecycle_unit, now), |
| next_state_transition_time); |
| } |
| |
| // Proactively discard the LifecycleUnit that has been hidden for the longest |
| // time if it at least GetTimeInBackgroundBeforeProactiveDiscard() of Chrome |
| // usage time has elapsed since it was hidden. |
| // |
| // Note: Discarding a LifecycleUnit might change the value returned by |
| // GetTimeInBackgroundBeforeProactiveDiscard(). Therefore, discard only the |
| // oldest LifecycleUnit, rather than discarding all LifecycleUnits that have |
| // been non-visible long enough. If a discard happens, |
| // MaybeDiscardLifecycleUnit() returns a zero TimeTicks and another call to |
| // PerformStateTransitions() is scheduled immediately to check if another |
| // discard should happen. |
| if (oldest_discardable_lifecycle_unit && ShouldProactivelyDiscardTabs()) { |
| next_state_transition_time = std::min( |
| MaybeDiscardLifecycleUnit(oldest_discardable_lifecycle_unit, now), |
| next_state_transition_time); |
| } |
| |
| // Schedule the next call to PerformStateTransitions(). |
| DCHECK(!state_transitions_timer_->IsRunning()); |
| if (!next_state_transition_time.is_max()) |
| SchedulePerformStateTransitions(next_state_transition_time - now); |
| } |
| |
| base::TimeTicks TabManager::MaybeFreezeLifecycleUnit( |
| LifecycleUnit* lifecycle_unit, |
| base::TimeTicks now) { |
| DecisionDetails freeze_details; |
| if (!lifecycle_unit->CanFreeze(&freeze_details)) |
| return base::TimeTicks::Max(); |
| |
| const base::TimeTicks freeze_time = |
| std::max(lifecycle_unit->GetWallTimeWhenHidden() + |
| proactive_freeze_discard_params_.freeze_timeout, |
| // Do not refreeze a tab before the refreeze timeout has expired. |
| lifecycle_unit->GetStateChangeTime() + |
| proactive_freeze_discard_params_.refreeze_timeout); |
| |
| if (now >= freeze_time) { |
| lifecycle_unit->Freeze(); |
| return base::TimeTicks::Max(); |
| } |
| |
| return freeze_time; |
| } |
| |
| base::TimeTicks TabManager::MaybeUnfreezeLifecycleUnit( |
| LifecycleUnit* lifecycle_unit, |
| base::TimeTicks now) { |
| DCHECK_EQ(lifecycle_unit->GetState(), LifecycleUnitState::FROZEN); |
| |
| const base::TimeTicks unfreeze_time = std::max( |
| lifecycle_unit->GetStateChangeTime() + |
| proactive_freeze_discard_params_.unfreeze_timeout, |
| last_unfreeze_time_ + proactive_freeze_discard_params_.refreeze_timeout); |
| |
| if (now >= unfreeze_time) { |
| last_unfreeze_time_ = now; |
| lifecycle_unit->Unfreeze(); |
| return now + proactive_freeze_discard_params_.refreeze_timeout; |
| } |
| |
| return unfreeze_time; |
| } |
| |
| base::TimeTicks TabManager::MaybeDiscardLifecycleUnit( |
| LifecycleUnit* lifecycle_unit, |
| base::TimeTicks now) { |
| const base::TimeDelta usage_time_not_visible = |
| usage_clock_.GetTotalUsageTime() - |
| lifecycle_unit->GetChromeUsageTimeWhenHidden(); |
| const base::TimeDelta time_until_discard = |
| GetTimeInBackgroundBeforeProactiveDiscard() - usage_time_not_visible; |
| |
| if (time_until_discard <= base::TimeDelta()) { |
| lifecycle_unit->Discard(LifecycleUnitDiscardReason::PROACTIVE); |
| // Request another call to check if another discard should happen. |
| return base::TimeTicks(); |
| } |
| |
| if (usage_clock_.IsInUse()) |
| return now + time_until_discard; |
| |
| return base::TimeTicks::Max(); |
| } |
| |
| void TabManager::OnLifecycleUnitStateChanged( |
| LifecycleUnit* lifecycle_unit, |
| LifecycleUnitState last_state, |
| LifecycleUnitStateChangeReason reason) { |
| LifecycleUnitState state = lifecycle_unit->GetState(); |
| bool was_discarded = (last_state == LifecycleUnitState::PENDING_DISCARD || |
| last_state == LifecycleUnitState::DISCARDED); |
| bool is_discarded = (state == LifecycleUnitState::PENDING_DISCARD || |
| state == LifecycleUnitState::DISCARDED); |
| |
| if (is_discarded && !was_discarded) { |
| num_loaded_lifecycle_units_--; |
| } else if (was_discarded && !is_discarded) { |
| num_loaded_lifecycle_units_++; |
| // Incrementing the number of loaded tabs might change the return value of |
| // GetTimeInBackgroundBeforeProactiveDiscard(). Schedule a call to |
| // PerformStateTransitions() to determine if a tab should be discarded in |
| // response to that change. |
| SchedulePerformStateTransitions(base::TimeDelta()); |
| } |
| |
| DCHECK_EQ(num_loaded_lifecycle_units_, |
| GetNumLoadedLifecycleUnits(lifecycle_units_)); |
| } |
| |
| void TabManager::OnLifecycleUnitVisibilityChanged( |
| LifecycleUnit* lifecycle_unit, |
| content::Visibility visibility) { |
| SchedulePerformStateTransitions(base::TimeDelta()); |
| } |
| |
| void TabManager::OnLifecycleUnitDestroyed(LifecycleUnit* lifecycle_unit) { |
| if (lifecycle_unit->GetState() != LifecycleUnitState::DISCARDED && |
| lifecycle_unit->GetState() != LifecycleUnitState::PENDING_DISCARD) { |
| num_loaded_lifecycle_units_--; |
| } |
| lifecycle_units_.erase(lifecycle_unit); |
| |
| DCHECK_EQ(num_loaded_lifecycle_units_, |
| GetNumLoadedLifecycleUnits(lifecycle_units_)); |
| |
| SchedulePerformStateTransitions(base::TimeDelta()); |
| } |
| |
| void TabManager::OnLifecycleUnitCreated(LifecycleUnit* lifecycle_unit) { |
| lifecycle_units_.insert(lifecycle_unit); |
| if (lifecycle_unit->GetState() != LifecycleUnitState::DISCARDED) |
| num_loaded_lifecycle_units_++; |
| |
| // Add an observer to be notified of destruction. |
| lifecycle_unit->AddObserver(this); |
| |
| DCHECK_EQ(num_loaded_lifecycle_units_, |
| GetNumLoadedLifecycleUnits(lifecycle_units_)); |
| |
| SchedulePerformStateTransitions(base::TimeDelta()); |
| } |
| |
| bool TabManager::ShouldProactivelyDiscardTabs() { |
| if (!proactive_freeze_discard_params_.should_proactively_discard) |
| return false; |
| |
| // Don't proactively discard tabs while offline. |
| if (net::NetworkChangeNotifier::IsOffline()) |
| return false; |
| |
| return true; |
| } |
| |
| } // namespace resource_coordinator |