| // Copyright 2018 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/session_restore_policy.h" |
| |
| #include <math.h> |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_forward.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/no_destructor.h" |
| #include "base/sequence_checker.h" |
| #include "base/stl_util.h" |
| #include "base/system/sys_info.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "chrome/browser/engagement/site_engagement_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_features.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/performance_manager/public/decorators/site_data_recorder.h" |
| #include "components/performance_manager/public/graph/graph.h" |
| #include "components/performance_manager/public/performance_manager.h" |
| #include "components/performance_manager/public/persistence/site_data/site_data_reader.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.h" |
| |
| #if !defined(OS_ANDROID) |
| #include "chrome/browser/permissions/permission_manager_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "components/permissions/permission_manager.h" |
| #include "components/permissions/permission_result.h" |
| #endif |
| |
| namespace resource_coordinator { |
| |
| namespace { |
| |
| bool IsApp(content::WebContents* contents) { |
| static constexpr char kInternalUrlPrefix[] = "chrome-extension://"; |
| const GURL& url = contents->GetLastCommittedURL(); |
| return strncmp(url.spec().c_str(), kInternalUrlPrefix, |
| base::size(kInternalUrlPrefix)); |
| } |
| |
| bool IsInternalPage(content::WebContents* contents) { |
| static constexpr char kInternalUrlPrefix[] = "chrome://"; |
| const GURL& url = contents->GetLastCommittedURL(); |
| return strncmp(url.spec().c_str(), kInternalUrlPrefix, |
| base::size(kInternalUrlPrefix)); |
| } |
| |
| class SysInfoDelegate : public SessionRestorePolicy::Delegate { |
| public: |
| SysInfoDelegate() {} |
| ~SysInfoDelegate() override {} |
| |
| size_t GetNumberOfCores() const override { |
| return base::SysInfo::NumberOfProcessors(); |
| } |
| |
| size_t GetFreeMemoryMiB() const override { |
| constexpr int64_t kMibibytesInBytes = 1 << 20; |
| int64_t free_mem = |
| base::SysInfo::AmountOfAvailablePhysicalMemory() / kMibibytesInBytes; |
| DCHECK(free_mem >= 0); |
| return free_mem; |
| } |
| |
| base::TimeTicks NowTicks() const override { return base::TimeTicks::Now(); } |
| |
| size_t GetSiteEngagementScore(content::WebContents* contents) const override { |
| // Get the active navigation entry. Restored tabs should always have one. |
| auto& controller = contents->GetController(); |
| auto* nav_entry = |
| controller.GetEntryAtIndex(controller.GetCurrentEntryIndex()); |
| DCHECK(nav_entry); |
| |
| auto* engagement_svc = SiteEngagementService::Get( |
| Profile::FromBrowserContext(contents->GetBrowserContext())); |
| double engagement = |
| engagement_svc->GetDetails(nav_entry->GetURL()).total_score; |
| |
| // Return the engagement as an integer. |
| return engagement; |
| } |
| |
| static SysInfoDelegate* Get() { |
| static base::NoDestructor<SysInfoDelegate> delegate; |
| return delegate.get(); |
| } |
| }; |
| |
| } // namespace |
| |
| #if !defined(OS_ANDROID) |
| class TabDataAccess { |
| public: |
| using TabData = SessionRestorePolicy::TabData; |
| |
| // Instances of this class shouldn't be created, it should only be used via |
| // its static methods. |
| TabDataAccess() = delete; |
| ~TabDataAccess() = delete; |
| TabDataAccess(const TabDataAccess&) = delete; |
| TabDataAccess& operator=(const TabDataAccess&) = delete; |
| |
| // Schedule the task that will initialize |TabData::used_in_bg| from the site |
| // data database. This will schedule a call to |OnSiteDataLoaded| once the |
| // data is available. |
| static void SetUsedInBgFromSiteDataDB( |
| base::WeakPtr<SessionRestorePolicy> policy, |
| TabData* tab_data, |
| content::WebContents* contents); |
| |
| // Set the |TabData::used_in_bg| bit based on the data returned from the site |
| // data database. |
| static void SetUsedInBgFromSiteData(TabData* tab_data, |
| content::WebContents* contents, |
| TabData::SiteDataReaderData reader_data); |
| |
| // Callback that is invoked when the SiteData associated with a WebContents is |
| // ready to use. This will initialize |tab_data->used_in_bg| to the proper |
| // value and call |DispatchNotifyAllTabsScoredIfNeeded|. |
| static void OnSiteDataAvailable(base::WeakPtr<SessionRestorePolicy> policy, |
| content::WebContents* contents, |
| TabData::SiteDataReaderData reader_data); |
| }; |
| |
| void TabDataAccess::SetUsedInBgFromSiteDataDB( |
| base::WeakPtr<SessionRestorePolicy> policy, |
| TabData* tab_data, |
| content::WebContents* contents) { |
| tab_data->used_in_bg_setter_cancel_callback.Reset(base::BindOnce( |
| &TabDataAccess::OnSiteDataAvailable, std::move(policy), contents)); |
| |
| auto call_on_graph_cb = base::BindOnce( |
| [](base::WeakPtr<performance_manager::PageNode> page_node, |
| base::OnceCallback<void(TabData::SiteDataReaderData)> reply_cb, |
| scoped_refptr<base::SequencedTaskRunner> reply_task_runner) { |
| if (!page_node) { |
| reply_task_runner->PostTask( |
| FROM_HERE, base::BindOnce(std::move(reply_cb), |
| TabData::SiteDataReaderData())); |
| return; |
| } |
| auto* reader = |
| performance_manager::SiteDataRecorder::Data::FromPageNode( |
| page_node.get()) |
| ->reader(); |
| // The tab won't have a reader if it doesn't have an URL tracked in the |
| // site data database. |
| if (!reader) { |
| reply_task_runner->PostTask( |
| FROM_HERE, base::BindOnce(std::move(reply_cb), |
| TabData::SiteDataReaderData())); |
| return; |
| } |
| |
| // The reader will call |reply_cb| once the data is available. |
| reader->RegisterDataLoadedCallback(base::BindOnce( |
| [](const performance_manager::SiteDataReader* reader, |
| base::OnceCallback<void(TabData::SiteDataReaderData)> reply_cb, |
| scoped_refptr<base::SequencedTaskRunner> reply_task_runner) { |
| static const performance_manager::SiteFeatureUsage kNotUsed = |
| performance_manager::SiteFeatureUsage::kSiteFeatureNotInUse; |
| TabData::SiteDataReaderData reader_data = {}; |
| reader_data.updates_favicon_in_bg = |
| reader->UpdatesFaviconInBackground() != kNotUsed; |
| reader_data.updates_title_in_bg = |
| reader->UpdatesTitleInBackground() != kNotUsed; |
| reply_task_runner->PostTask( |
| FROM_HERE, base::BindOnce(std::move(reply_cb), reader_data)); |
| }, |
| base::Unretained(reader), std::move(reply_cb), reply_task_runner)); |
| }, |
| performance_manager::PerformanceManager::GetPageNodeForWebContents( |
| contents), |
| tab_data->used_in_bg_setter_cancel_callback.callback(), |
| base::SequencedTaskRunnerHandle::Get()); |
| |
| performance_manager::PerformanceManager::CallOnGraph( |
| FROM_HERE, std::move(call_on_graph_cb)); |
| } |
| |
| void TabDataAccess::SetUsedInBgFromSiteData( |
| TabData* tab_data, |
| content::WebContents* contents, |
| TabData::SiteDataReaderData reader_data) { |
| // Determine if background communication with the user is used. A pinned tab |
| // has no visible tab title, so tab title updates can be ignored in that case. |
| // The audio bit is ignored as tab can't play audio until they have been |
| // visible at least once. We err on the side of caution, if unsure about a |
| // feature (usually because of a lack of observation) then the feature is |
| // considered as used. |
| bool used_in_bg = reader_data.updates_favicon_in_bg; |
| if (!tab_data->is_pinned && reader_data.updates_title_in_bg) |
| used_in_bg = true; |
| |
| auto notif_permission = |
| PermissionManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(contents->GetBrowserContext())) |
| ->GetPermissionStatus(ContentSettingsType::NOTIFICATIONS, |
| contents->GetLastCommittedURL(), |
| contents->GetLastCommittedURL()); |
| if (notif_permission.content_setting == CONTENT_SETTING_ALLOW) |
| used_in_bg = true; |
| |
| tab_data->used_in_bg = used_in_bg; |
| } |
| |
| void TabDataAccess::OnSiteDataAvailable( |
| base::WeakPtr<SessionRestorePolicy> policy, |
| content::WebContents* contents, |
| TabData::SiteDataReaderData reader_data) { |
| if (!policy) |
| return; |
| |
| auto it = policy->tab_data_.find(contents); |
| DCHECK(it != policy->tab_data_.end()); |
| auto* tab_data = it->second.get(); |
| |
| SetUsedInBgFromSiteData(tab_data, contents, reader_data); |
| |
| // Score the tab and notify observers if the score has changed. |
| if (policy->RescoreTabAfterDataLoaded(contents, tab_data)) |
| policy->notify_tab_score_changed_callback_.Run(contents, tab_data->score); |
| |
| ++policy->tabs_scored_; |
| DCHECK(tab_data->used_in_bg.has_value()); |
| if (tab_data->used_in_bg) |
| ++policy->tabs_used_in_bg_; |
| |
| policy->DispatchNotifyAllTabsScoredIfNeeded(); |
| } |
| #endif |
| |
| SessionRestorePolicy::SessionRestorePolicy() |
| : policy_enabled_(true), |
| delegate_(SysInfoDelegate::Get()), |
| simultaneous_tab_loads_(CalculateSimultaneousTabLoads()) {} |
| |
| SessionRestorePolicy::~SessionRestorePolicy() { |
| // Record the number of tabs involved in the session restore that use |
| // background communications mechanisms. |
| DCHECK_GE(tabs_used_in_bg_, tabs_used_in_bg_restored_); |
| UMA_HISTOGRAM_COUNTS_100("SessionRestore.BackgroundUseCaseTabCount.Total", |
| tabs_used_in_bg_); |
| UMA_HISTOGRAM_COUNTS_100("SessionRestore.BackgroundUseCaseTabCount.Restored", |
| tabs_used_in_bg_restored_); |
| } |
| |
| float SessionRestorePolicy::AddTabForScoring(content::WebContents* contents) { |
| DCHECK(!base::Contains(tab_data_, contents)); |
| |
| // When the first tab is added keep track of a 'now' time. This ensures that |
| // the scoring function returns consistent values over the lifetime of the |
| // policy object. |
| if (tab_data_.empty()) |
| now_ = delegate_->NowTicks(); |
| |
| auto iter = |
| tab_data_.insert(std::make_pair(contents, std::make_unique<TabData>())); |
| TabData* tab_data = iter.first->second.get(); |
| |
| // Determine if the tab is pinned. This is only defined on desktop platforms. |
| #if defined(OS_ANDROID) |
| tab_data->is_pinned = false; |
| #else |
| // TODO(chrisha): This is O(n^2) in the number of tabs being restored. Fix |
| // this! |
| // In theory all tabs should belong to a tab-strip, but in tests this isn't |
| // necessarily true. |
| auto* browser_list = BrowserList::GetInstance(); |
| for (size_t i = 0; i < browser_list->size(); ++i) { |
| auto* browser = browser_list->get(i); |
| auto* tab_strip = browser->tab_strip_model(); |
| int tab_index = tab_strip->GetIndexOfWebContents(contents); |
| if (tab_index == TabStripModel::kNoTab) |
| continue; |
| tab_data->is_pinned = tab_strip->IsTabPinned(tab_index); |
| break; |
| } |
| #endif // !defined(OS_ANDROID) |
| |
| // Cache a handful of other properties. |
| tab_data->is_app = IsApp(contents); |
| tab_data->is_internal = IsInternalPage(contents); |
| tab_data->site_engagement = delegate_->GetSiteEngagementScore(contents); |
| tab_data->last_active = now_ - contents->GetLastActiveTime(); |
| |
| // The local database doesn't exist on Android at all. |
| #if !defined(OS_ANDROID) |
| TabDataAccess::SetUsedInBgFromSiteDataDB(weak_factory_.GetWeakPtr(), tab_data, |
| contents); |
| #endif // !defined(OS_ANDROID) |
| |
| // Another tab has been added, so an existing all tabs scored notification may |
| // be required. |
| if (HasFinalScore(tab_data)) { |
| ++tabs_scored_; |
| if (notification_state_ == NotificationState::kDelivered) |
| notification_state_ = NotificationState::kNotSent; |
| DispatchNotifyAllTabsScoredIfNeeded(); |
| } else { |
| notification_state_ = NotificationState::kNotSent; |
| } |
| |
| ScoreTab(tab_data); |
| return tab_data->score; |
| } |
| |
| void SessionRestorePolicy::RemoveTabForScoring(content::WebContents* contents) { |
| auto it = tab_data_.find(contents); |
| DCHECK(it != tab_data_.end()); |
| auto* tab_data = it->second.get(); |
| |
| if (HasFinalScore(tab_data)) { |
| --tabs_scored_; |
| |
| // Tabs are removed from the policy engine when they start loading. |
| if (tab_data->UsedInBg()) |
| ++tabs_used_in_bg_restored_; |
| } |
| |
| tab_data_.erase(it); |
| DispatchNotifyAllTabsScoredIfNeeded(); |
| } |
| |
| bool SessionRestorePolicy::ShouldLoad(content::WebContents* contents) const { |
| // If the policy is disabled then always return true. |
| if (!policy_enabled_) |
| return true; |
| |
| if (tab_loads_started_ < min_tabs_to_restore_) |
| return true; |
| |
| if (max_tabs_to_restore_ != 0 && tab_loads_started_ >= max_tabs_to_restore_) |
| return false; |
| |
| // If there is a free memory constraint then enforce it. |
| if (mb_free_memory_per_tab_to_restore_ != 0) { |
| size_t free_mem_mb = delegate_->GetFreeMemoryMiB(); |
| if (free_mem_mb < mb_free_memory_per_tab_to_restore_) |
| return false; |
| } |
| |
| auto it = tab_data_.find(contents); |
| DCHECK(it != tab_data_.end()); |
| const TabData* tab_data = it->second.get(); |
| |
| // Enforce a max time since use if one is specified. |
| if (!max_time_since_last_use_to_restore_.is_zero()) { |
| base::TimeDelta time_since_active = |
| delegate_->NowTicks() - contents->GetLastActiveTime(); |
| if (time_since_active > max_time_since_last_use_to_restore_) |
| return false; |
| } |
| |
| // Only enforce the site engagement score for tabs that don't make use of |
| // background communication mechanisms. These sites often have low engagements |
| // because they are only used very sporadically, but it is important that they |
| // are loaded because if not loaded the user can miss important messages. |
| bool enforce_site_engagement_score = true; |
| if (tab_data->UsedInBg()) |
| enforce_site_engagement_score = false; |
| |
| // Enforce a minimum site engagement score if applicable. |
| if (enforce_site_engagement_score && |
| tab_data->site_engagement < min_site_engagement_to_restore_) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SessionRestorePolicy::NotifyTabLoadStarted() { |
| ++tab_loads_started_; |
| } |
| |
| SessionRestorePolicy::SessionRestorePolicy(bool policy_enabled, |
| const Delegate* delegate) |
| : policy_enabled_(policy_enabled), |
| delegate_(delegate), |
| simultaneous_tab_loads_(CalculateSimultaneousTabLoads()) {} |
| |
| // static |
| size_t SessionRestorePolicy::CalculateSimultaneousTabLoads( |
| size_t min_loads, |
| size_t max_loads, |
| size_t cores_per_load, |
| size_t num_cores) { |
| DCHECK(max_loads == 0 || min_loads <= max_loads); |
| DCHECK(num_cores > 0); |
| |
| size_t loads = 0; |
| |
| // Setting |cores_per_load| == 0 means that no per-core limit is applied. |
| if (cores_per_load == 0) { |
| loads = std::numeric_limits<size_t>::max(); |
| } else { |
| loads = num_cores / cores_per_load; |
| } |
| |
| // If |max_loads| isn't zero then apply the maximum that it implies. |
| if (max_loads != 0) |
| loads = std::min(loads, max_loads); |
| |
| loads = std::max(loads, min_loads); |
| |
| return loads; |
| } |
| |
| size_t SessionRestorePolicy::CalculateSimultaneousTabLoads() const { |
| // If the policy is disabled then there are no limits on the simultaneous tab |
| // loads. |
| if (!policy_enabled_) |
| return std::numeric_limits<size_t>::max(); |
| return CalculateSimultaneousTabLoads( |
| min_simultaneous_tab_loads_, max_simultaneous_tab_loads_, |
| cores_per_simultaneous_tab_load_, delegate_->GetNumberOfCores()); |
| } |
| |
| void SessionRestorePolicy::DispatchNotifyAllTabsScoredIfNeeded() { |
| // If a notification has already been sent then there's no need to send |
| // another. |
| if (notification_state_ == NotificationState::kDelivered) |
| return; |
| |
| if (tabs_scored_ != tab_data_.size()) { |
| // An enroute notification should be canceled, as its no longer valid. |
| notification_state_ = NotificationState::kNotSent; |
| return; |
| } |
| |
| // A notification is already enroute, no need to send another. |
| if (notification_state_ == NotificationState::kEnRoute) |
| return; |
| |
| // This is done asynchronously so that this notification doesn't arrive before |
| // a tab score is delivered. |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(&SessionRestorePolicy::NotifyAllTabsScored, |
| weak_factory_.GetWeakPtr())); |
| notification_state_ = NotificationState::kEnRoute; |
| } |
| |
| void SessionRestorePolicy::NotifyAllTabsScored() { |
| // Only deliver the notification if its still desired; pending notifications |
| // can be canceled as conditions change. |
| if (notification_state_ != NotificationState::kEnRoute) |
| return; |
| notification_state_ = NotificationState::kDelivered; |
| // This callback can indirectly cause our parent to release us, so make it the |
| // last thing we do to avoid a use after free. crbug.com/946863 |
| notify_tab_score_changed_callback_.Run(nullptr, 0.0); |
| } |
| |
| bool SessionRestorePolicy::RescoreTabAfterDataLoaded( |
| content::WebContents* contents /* unused */, |
| TabData* tab_data) { |
| return ScoreTab(tab_data); |
| } |
| |
| // static |
| bool SessionRestorePolicy::ScoreTab(TabData* tab_data) { |
| float score = 0.0f; |
| |
| // Give higher priorities to tabs used in the background, and lowest |
| // priority to internal tabs. Apps and pinned tabs are simply treated as |
| // normal tabs. |
| if (tab_data->UsedInBg()) { |
| score = 2; |
| } else if (!tab_data->is_internal) { |
| score = 1; |
| } |
| |
| // Refine the score using the age of the tab. More recently used tabs have |
| // higher scores. |
| score += CalculateAgeScore(tab_data); |
| |
| if (score == tab_data->score) |
| return false; |
| |
| tab_data->score = score; |
| return true; |
| } |
| |
| // static |
| float SessionRestorePolicy::CalculateAgeScore(const TabData* tab_data) { |
| // Convert the age into seconds. Cap absolute values less than 1 so that |
| // the inverse will be between -1 and 1. |
| double score = tab_data->last_active.InSecondsF(); |
| if (fabs(score) < 1.0f) { |
| if (score > 0) |
| score = 1; |
| else |
| score = -1; |
| } |
| DCHECK_LE(1.0f, fabs(score)); |
| |
| // Invert the score (1 / score). |
| // Really old (infinity) maps to 0 (lowest priority). |
| // Really young positive age (1) maps to 1 (moderate priority). |
| // A little in the future (-1) maps to -1 (moderate priority). |
| // Really far in the future (-infinity) maps to 0 (highest priority). |
| // Shifting negative scores from [-1, 0] to [1, 2] keeps the scores increasing |
| // with priority. |
| if (score < 0) { |
| score = 2.0 + 1.0 / score; |
| } else { |
| score = 1.0 / score; |
| } |
| DCHECK_LE(0.0, score); |
| DCHECK_GE(2.0, score); |
| |
| // Rescale the age score to the range [0, 1] so that it can be added to the |
| // category scores already calculated. Divide by 2 + epsilon so that no |
| // score will end up rounding up to 1.0, but instead be capped at 0.999. |
| score /= 2.002; |
| DCHECK_LE(0.0, score); |
| DCHECK_GT(1.0, score); |
| |
| return score; |
| } |
| |
| // static |
| bool SessionRestorePolicy::HasFinalScore(const TabData* tab_data) { |
| return tab_data->used_in_bg.has_value(); |
| } |
| |
| void SessionRestorePolicy::SetTabLoadsStartedForTesting( |
| size_t tab_loads_started) { |
| tab_loads_started_ = tab_loads_started; |
| } |
| |
| void SessionRestorePolicy::UpdateSiteEngagementScoreForTesting( |
| content::WebContents* contents, |
| size_t score) { |
| auto it = tab_data_.find(contents); |
| it->second->site_engagement = score; |
| } |
| |
| SessionRestorePolicy::Delegate::Delegate() {} |
| |
| SessionRestorePolicy::Delegate::~Delegate() {} |
| |
| SessionRestorePolicy::TabData::TabData() = default; |
| |
| SessionRestorePolicy::TabData::~TabData() { |
| used_in_bg_setter_cancel_callback.Cancel(); |
| } |
| |
| bool SessionRestorePolicy::TabData::UsedInBg() const { |
| return used_in_bg.has_value() && used_in_bg.value(); |
| } |
| |
| } // namespace resource_coordinator |