blob: 673928cd1facfb28b7f9a78075d06d2a84dc238e [file] [log] [blame]
// 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