blob: f1748d0b2784cfdbd3d6be082fdfae85c5a60aca [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 <memory>
#include <set>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/feature_list.h"
#include "base/memory/memory_pressure_monitor.h"
#include "base/memory/raw_ptr.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/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/task/post_task.h"
#include "base/threading/thread.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/traced_value.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.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/resource_coordinator_parts.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/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/public/features.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/browser_task_traits.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 "net/base/network_change_notifier.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/resource_coordinator/tab_manager_delegate_chromeos.h"
#endif
using base::TimeTicks;
using content::BrowserThread;
using content::WebContents;
namespace resource_coordinator {
namespace {
using LoadingState = TabLoadTracker::LoadingState;
} // 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:
raw_ptr<TabManager> tab_manager_;
};
TabManager::TabManager(TabLoadTracker* tab_load_tracker)
: browser_tab_strip_tracker_(this, nullptr),
is_session_restore_loading_tabs_(false),
restored_tab_count_(0u),
tab_load_tracker_(tab_load_tracker) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
delegate_ =
std::make_unique<TabManagerDelegate>(weak_ptr_factory_.GetWeakPtr());
#endif
browser_tab_strip_tracker_.Init();
session_restore_observer_ =
std::make_unique<TabManagerSessionRestoreObserver>(this);
stats_collector_ = std::make_unique<TabManagerStatsCollector>();
tab_load_tracker_->AddObserver(this);
}
TabManager::~TabManager() {
tab_load_tracker_->RemoveObserver(this);
}
void TabManager::Start() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
delegate_->StartPeriodicOOMScoreUpdate();
#endif
// MemoryPressureMonitor is not implemented on Linux so far and tabs are never
// discarded.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
// Don't handle memory pressure events here if this is done by
// PerformanceManager.
if (!base::FeatureList::IsEnabled(
performance_manager::features::
kUrgentDiscardingFromPerformanceManager)) {
// 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) {
RegisterMemoryPressureListener();
base::MemoryPressureListener::MemoryPressureLevel level =
monitor->GetCurrentPressureLevel();
if (level ==
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
OnMemoryPressure(level);
}
}
}
#endif
// Create the graph observer. This is the source of page almost idle data and
// EQT measurements.
// TODO(sebmarchand): Remove the "IsAvailable" check, or merge the TM into the
// PM. The TM and PM must always exist together.
if (performance_manager::PerformanceManagerImpl::IsAvailable()) {
performance_manager::PerformanceManagerImpl::CallOnGraphImpl(
FROM_HERE, base::BindOnce(
[](std::unique_ptr<ResourceCoordinatorSignalObserver>
rc_signal_observer,
performance_manager::GraphImpl* graph) {
graph->PassToGraph(std::move(rc_signal_observer));
},
std::make_unique<ResourceCoordinatorSignalObserver>(
weak_ptr_factory_.GetWeakPtr())));
}
g_browser_process->resource_coordinator_parts()
->tab_lifecycle_unit_source()
->Start();
}
LifecycleUnitVector TabManager::GetSortedLifecycleUnits() {
LifecycleUnitVector sorted_lifecycle_units(lifecycle_units_.begin(),
lifecycle_units_.end());
// Sort lifecycle_units with ascending importance.
std::sort(sorted_lifecycle_units.begin(), sorted_lifecycle_units.end(),
[](LifecycleUnit* a, LifecycleUnit* b) {
return a->GetSortKey() < b->GetSortKey();
});
return sorted_lifecycle_units;
}
void TabManager::DiscardTab(LifecycleUnitDiscardReason reason,
TabDiscardDoneCB tab_discard_done) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Call Chrome OS specific low memory handling process.
delegate_->LowMemoryKill(reason, std::move(tab_discard_done));
#else
DiscardTabImpl(reason, std::move(tab_discard_done));
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
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(
LifecycleUnitDiscardReason::EXTERNAL)) {
return tab_lifecycle_unit_external->GetWebContents();
}
return nullptr;
}
return DiscardTabImpl(LifecycleUnitDiscardReason::EXTERNAL);
}
void TabManager::DiscardTabFromMemoryPressure() {
DCHECK(!base::FeatureList::IsEnabled(
performance_manager::features::kUrgentDiscardingFromPerformanceManager));
#if BUILDFLAG(IS_CHROMEOS)
// Output a log with per-process memory usage and number of file descriptors,
// as well as GPU memory details. Discard happens without waiting for the log
// (https://crbug.com/850545) Per comment at
// https://crrev.com/c/chromium/src/+/1980282/3#message-d45cc354e7776d7e3d208e22dd2f6bbca3e9eae8,
// this log is used to diagnose issues on ChromeOS. Do not output it on other
// platforms since it is not used and data shows it can create IO thread hangs
// (https://crbug.com/1040522).
memory::OomMemoryDetails::Log("Tab Discards Memory details");
#endif // BUILDFLAG(IS_CHROMEOS)
// Start handling memory pressure. Suppress further notifications before
// completion in case a slow handler queues up multiple dispatches of this
// method and inadvertently discards more than necessary tabs/apps in a burst.
UnregisterMemoryPressureListener();
TabDiscardDoneCB tab_discard_done(base::BindOnce(
&TabManager::OnTabDiscardDone, weak_ptr_factory_.GetWeakPtr()));
DiscardTab(LifecycleUnitDiscardReason::URGENT, std::move(tab_discard_done));
}
void TabManager::AddObserver(TabLifecycleObserver* observer) {
TabLifecycleUnitExternal::AddTabLifecycleObserver(observer);
}
void TabManager::RemoveObserver(TabLifecycleObserver* observer) {
TabLifecycleUnitExternal::RemoveTabLifecycleObserver(observer);
}
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
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 < base::size(kInternalPagePrefixes); ++i) {
if (!strncmp(url.spec().c_str(), kInternalPagePrefixes[i],
strlen(kInternalPagePrefixes[i])))
return true;
}
return false;
}
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:
DiscardTabFromMemoryPressure();
return;
}
NOTREACHED();
}
void TabManager::OnTabDiscardDone() {
base::MemoryPressureMonitor* monitor = base::MemoryPressureMonitor::Get();
if (!monitor)
return;
// Create a MemoryPressureListener instance to re-register to the observer.
// Note that we've just finished handling memory pressure and async
// tab/app discard might haven't taken effect yet. Don't check memory pressure
// level or act on it, or we might over-discard tabs or apps.
RegisterMemoryPressureListener();
}
void TabManager::RegisterMemoryPressureListener() {
DCHECK(!memory_pressure_listener_);
// Use sync memory pressure listener.
memory_pressure_listener_ = std::make_unique<base::MemoryPressureListener>(
FROM_HERE, base::BindRepeating(&TabManager::OnMemoryPressure,
weak_ptr_factory_.GetWeakPtr()));
}
void TabManager::UnregisterMemoryPressureListener() {
// Destroying the memory pressure listener to unregister from the observer.
memory_pressure_listener_.reset();
}
void TabManager::OnActiveTabChanged(content::WebContents* old_contents,
content::WebContents* new_contents) {
// If |old_contents| is set, that tab has switched from being active to
// inactive, so record the time of that transition.
if (old_contents) {
// Only record switch-to-tab metrics when a switch happens, i.e.
// |old_contents| is set.
stats_collector_->RecordSwitchToTab(old_contents, new_contents);
}
}
void TabManager::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (change.type() == TabStripModelChange::kReplaced) {
auto* replace = change.GetReplace();
WebContentsData::CopyState(replace->old_contents, 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) {
stats_collector_->OnTabIsLoaded(web_contents);
}
}
void TabManager::OnStopTracking(content::WebContents* web_contents,
LoadingState loading_state) {
GetWebContentsData(web_contents)->SetTabLoadingState(loading_state);
}
// 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,
TabDiscardDoneCB tab_discard_done) {
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);
}
void TabManager::OnWebContentsDestroyed(content::WebContents* contents) {
stats_collector_->OnWebContentsDestroyed(contents);
}
void TabManager::OnLifecycleUnitDestroyed(LifecycleUnit* lifecycle_unit) {
lifecycle_units_.erase(lifecycle_unit);
}
void TabManager::OnLifecycleUnitCreated(LifecycleUnit* lifecycle_unit) {
// Add an observer to be notified of destruction.
lifecycle_unit->AddObserver(this);
lifecycle_units_.insert(lifecycle_unit);
}
} // namespace resource_coordinator