| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/webui/discards/discards_ui.h" |
| |
| #include <algorithm> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/containers/flat_map.h" |
| #include "base/containers/to_vector.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/notreached.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/android_buildflags.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/performance_manager/policies/discard_eligibility_policy.h" |
| #include "chrome/browser/performance_manager/public/user_tuning/performance_detection_manager.h" |
| #include "chrome/browser/performance_manager/public/user_tuning/user_tuning_utils.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/time.h" |
| #include "chrome/browser/ui/webui/discards/discards.mojom.h" |
| #include "chrome/browser/ui/webui/discards/graph_dump_impl.h" |
| #include "chrome/browser/ui/webui/discards/site_data.mojom-forward.h" |
| #include "chrome/browser/ui/webui/discards/site_data_provider_impl.h" |
| #include "chrome/browser/ui/webui/favicon_source.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/grit/discards_resources.h" |
| #include "chrome/grit/discards_resources_map.h" |
| #include "components/favicon_base/favicon_url_parser.h" |
| #include "components/performance_manager/public/decorators/page_live_state_decorator.h" |
| #include "components/performance_manager/public/features.h" |
| #include "components/performance_manager/public/freezing/cannot_freeze_reason.h" |
| #include "components/performance_manager/public/freezing/freezing.h" |
| #include "components/performance_manager/public/graph/graph.h" |
| #include "components/performance_manager/public/graph/page_node.h" |
| #include "components/performance_manager/public/performance_manager.h" |
| #include "components/performance_manager/public/user_tuning/prefs.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/site_engagement/content/site_engagement_service.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/url_data_source.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_ui.h" |
| #include "content/public/browser/web_ui_data_source.h" |
| #include "content/public/browser/web_ui_message_handler.h" |
| #include "content/public/common/content_features.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "services/network/public/mojom/content_security_policy.mojom.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "ui/resources/grit/ui_resources_map.h" |
| #include "ui/webui/webui_util.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| #if !BUILDFLAG(IS_DESKTOP_ANDROID) |
| #include "chrome/browser/resource_coordinator/lifecycle_unit.h" |
| #include "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h" |
| #endif // !BUILDFLAG(IS_DESKTOP_ANDROID) |
| |
| using performance_manager::PageNode; |
| using performance_manager::policies::DiscardEligibilityPolicy; |
| |
| namespace { |
| |
| discards::mojom::LifecycleUnitVisibility GetLifecycleUnitVisibility( |
| content::Visibility visibility) { |
| switch (visibility) { |
| case content::Visibility::HIDDEN: |
| return discards::mojom::LifecycleUnitVisibility::HIDDEN; |
| case content::Visibility::OCCLUDED: |
| return discards::mojom::LifecycleUnitVisibility::OCCLUDED; |
| case content::Visibility::VISIBLE: |
| return discards::mojom::LifecycleUnitVisibility::VISIBLE; |
| } |
| #if defined(COMPILER_MSVC) |
| NOTREACHED(); |
| #endif |
| } |
| |
| double GetSiteEngagementScore(content::WebContents* contents) { |
| // Get the active navigation entry. Restored tabs should always have one. |
| auto& controller = contents->GetController(); |
| const int current_entry_index = controller.GetCurrentEntryIndex(); |
| |
| // A WebContents which hasn't navigated yet does not have a NavigationEntry. |
| if (current_entry_index == -1) { |
| return 0; |
| } |
| |
| auto* nav_entry = controller.GetEntryAtIndex(current_entry_index); |
| DCHECK(nav_entry); |
| |
| auto* engagement_svc = site_engagement::SiteEngagementService::Get( |
| Profile::FromBrowserContext(contents->GetBrowserContext())); |
| return engagement_svc->GetDetails(nav_entry->GetURL()).total_score; |
| } |
| |
| mojom::LifecycleUnitLoadingState GetLifecycleUnitLoadingState( |
| PageNode::LoadingState loading_state) { |
| switch (loading_state) { |
| case PageNode::LoadingState::kLoadingNotStarted: |
| case PageNode::LoadingState::kLoadingTimedOut: |
| return mojom::LifecycleUnitLoadingState::UNLOADED; |
| |
| case PageNode::LoadingState::kLoading: |
| return mojom::LifecycleUnitLoadingState::LOADING; |
| |
| case PageNode::LoadingState::kLoadedBusy: |
| case PageNode::LoadingState::kLoadedIdle: |
| return mojom::LifecycleUnitLoadingState::LOADED; |
| } |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| discards::mojom::CanFreeze ToCanFreezeMojom( |
| performance_manager::freezing::CanFreeze can_freeze) { |
| switch (can_freeze) { |
| case performance_manager::freezing::CanFreeze::kYes: |
| return discards::mojom::CanFreeze::YES; |
| case performance_manager::freezing::CanFreeze::kNo: |
| return discards::mojom::CanFreeze::NO; |
| case performance_manager::freezing::CanFreeze::kVaries: |
| return discards::mojom::CanFreeze::VARIES; |
| } |
| NOTREACHED(); |
| } |
| |
| std::vector<std::string> ToCannotFreezeReasonsStrings( |
| const performance_manager::freezing::CanFreezeDetails& details) { |
| std::vector<std::string> reasons; |
| reasons.reserve(details.cannot_freeze_reasons.size() + |
| details.cannot_freeze_reasons_connected_pages.size()); |
| for (auto reason : details.cannot_freeze_reasons) { |
| reasons.push_back( |
| performance_manager::freezing::CannotFreezeReasonToString(reason)); |
| } |
| for (auto reason : details.cannot_freeze_reasons_connected_pages) { |
| reasons.push_back(base::StringPrintf( |
| "%s (from connected page)", |
| performance_manager::freezing::CannotFreezeReasonToString(reason))); |
| } |
| return reasons; |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| class DiscardsDetailsProviderImpl |
| : public discards::mojom::DetailsProvider, |
| public performance_manager::GraphOwnedDefaultImpl { |
| public: |
| // This instance is deleted when the supplied pipe is destroyed. |
| explicit DiscardsDetailsProviderImpl( |
| mojo::PendingReceiver<discards::mojom::DetailsProvider> receiver) |
| : receiver_(this, std::move(receiver)) {} |
| |
| DiscardsDetailsProviderImpl(const DiscardsDetailsProviderImpl&) = delete; |
| DiscardsDetailsProviderImpl& operator=(const DiscardsDetailsProviderImpl&) = |
| delete; |
| |
| ~DiscardsDetailsProviderImpl() override = default; |
| |
| // discards::mojom::DetailsProvider overrides: |
| void GetTabDiscardsInfo(GetTabDiscardsInfoCallback callback) override { |
| std::vector<discards::mojom::TabDiscardsInfoPtr> infos; |
| |
| DiscardEligibilityPolicy* eligiblity_policy = |
| DiscardEligibilityPolicy::GetFromGraph(GetOwningGraph()); |
| DCHECK(eligiblity_policy); |
| |
| std::vector<performance_manager::policies::PageNodeSortProxy> candidates; |
| for (const PageNode* page_node : GetOwningGraph()->GetAllPageNodes()) { |
| if (page_node->GetType() != performance_manager::PageType::kTab) { |
| continue; |
| } |
| performance_manager::policies::CanDiscardResult can_discard_result = |
| eligiblity_policy->CanDiscard( |
| page_node, DiscardEligibilityPolicy::DiscardReason::URGENT); |
| candidates.emplace_back(page_node->GetWeakPtr(), can_discard_result, |
| page_node->IsVisible(), page_node->IsFocused(), |
| page_node->GetLastVisibilityChangeTime()); |
| } |
| |
| // Sorts with ascending importance. |
| std::sort(candidates.begin(), candidates.end()); |
| |
| page_nodes_by_id_.clear(); |
| |
| int32_t rank = 1; |
| int32_t id = 1; |
| for (auto& candidate : candidates) { |
| discards::mojom::TabDiscardsInfoPtr info( |
| discards::mojom::TabDiscardsInfo::New()); |
| |
| const base::WeakPtr<const PageNode> page_node = candidate.page_node(); |
| content::WebContents* contents = page_node->GetWebContents().get(); |
| CHECK(contents); |
| |
| info->tab_url = contents->GetLastCommittedURL().spec(); |
| info->title = base::UTF16ToUTF8(contents->GetTitle()); |
| info->visibility = GetLifecycleUnitVisibility(contents->GetVisibility()); |
| info->loading_state = |
| GetLifecycleUnitLoadingState(page_node->GetLoadingState()); |
| |
| info->cannot_discard_reasons = |
| performance_manager::user_tuning::GetCannotDiscardReasonsForPageNode( |
| page_node.get()); |
| info->can_discard = info->cannot_discard_reasons.empty(); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| info->cannot_freeze_reasons = {"not implemented"}; |
| info->can_freeze = discards::mojom::CanFreeze::NO; |
| #else |
| // TODO(crbug.com/40160563): Add FreezingPolicy to Android. |
| const auto can_freeze_details = |
| performance_manager::freezing::GetCanFreezeDetailsForPageNode( |
| page_node.get()); |
| info->cannot_freeze_reasons = |
| ToCannotFreezeReasonsStrings(can_freeze_details); |
| info->can_freeze = ToCanFreezeMojom(can_freeze_details.can_freeze); |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| info->utility_rank = rank++; |
| info->id = id++; |
| page_nodes_by_id_.insert(std::make_pair(info->id, page_node)); |
| const auto* live_state_data = |
| performance_manager::PageLiveStateDecorator::Data::FromPageNode( |
| page_node.get()); |
| if (live_state_data) { |
| info->is_auto_discardable = live_state_data->IsAutoDiscardable(); |
| } |
| info->site_engagement_score = GetSiteEngagementScore(contents); |
| info->has_focus = page_node->IsFocused(); |
| |
| #if !BUILDFLAG(IS_DESKTOP_ANDROID) |
| auto* lifecycle_unit_external = resource_coordinator:: |
| TabLifecycleUnitSource::GetTabLifecycleUnitExternal(contents); |
| // A TabLifecycleUnitExternal object is always a TabLifecycleUnit object. |
| // TabLifecycleUnit will be removed (crbug.com/394889323). |
| resource_coordinator::TabLifecycleUnitSource::TabLifecycleUnit* |
| lifecycle_unit = static_cast< |
| resource_coordinator::TabLifecycleUnitSource::TabLifecycleUnit*>( |
| lifecycle_unit_external); |
| if (lifecycle_unit) { |
| info->state = lifecycle_unit->GetState(); |
| info->discard_reason = lifecycle_unit->GetDiscardReason(); |
| info->discard_count = lifecycle_unit->GetDiscardCount(); |
| const base::TimeTicks last_focused_time = |
| lifecycle_unit->GetLastFocusedTimeTicks(); |
| const base::TimeDelta elapsed = |
| (last_focused_time == base::TimeTicks::Max()) |
| ? base::TimeDelta() |
| : (resource_coordinator::NowTicks() - last_focused_time); |
| info->last_active_seconds = static_cast<int32_t>(elapsed.InSeconds()); |
| info->state_change_time = |
| lifecycle_unit->GetStateChangeTime() - base::TimeTicks::UnixEpoch(); |
| } |
| #endif // !BUILDFLAG(IS_DESKTOP_ANDROID) |
| |
| infos.push_back(std::move(info)); |
| } |
| |
| std::move(callback).Run(std::move(infos)); |
| } |
| |
| void SetAutoDiscardable(int32_t id, |
| bool is_auto_discardable, |
| SetAutoDiscardableCallback callback) override { |
| auto it = page_nodes_by_id_.find(id); |
| if (it != page_nodes_by_id_.end()) { |
| content::WebContents* contents = it->second->GetWebContents().get(); |
| CHECK(contents); |
| performance_manager::PageLiveStateDecorator::SetIsAutoDiscardable( |
| contents, is_auto_discardable); |
| } |
| std::move(callback).Run(); |
| } |
| |
| void DiscardById(int32_t id, |
| mojom::LifecycleUnitDiscardReason reason, |
| DiscardByIdCallback callback) override { |
| auto it = page_nodes_by_id_.find(id); |
| if (it != page_nodes_by_id_.end() && it->second) { |
| const PageNode* page_node = it->second.get(); |
| performance_manager::user_tuning::DiscardPage( |
| page_node, reason, |
| /*ignore_minimum_time_in_background=*/true); |
| } |
| std::move(callback).Run(); |
| } |
| |
| void FreezeById(int32_t id) override { |
| auto it = page_nodes_by_id_.find(id); |
| if (it != page_nodes_by_id_.end() && it->second) { |
| const PageNode* page_node = it->second.get(); |
| content::WebContents* contents = page_node->GetWebContents().get(); |
| CHECK(contents); |
| contents->SetPageFrozen(true); |
| } |
| } |
| |
| void LoadById(int32_t id) override { |
| auto it = page_nodes_by_id_.find(id); |
| if (it != page_nodes_by_id_.end() && it->second) { |
| const PageNode* page_node = it->second.get(); |
| PageNode::LoadingState loading_state = page_node->GetLoadingState(); |
| if (loading_state != PageNode::LoadingState::kLoadingNotStarted && |
| loading_state != PageNode::LoadingState::kLoadingTimedOut) { |
| return; |
| } |
| |
| content::WebContents* contents = page_node->GetWebContents().get(); |
| CHECK(contents); |
| contents->GetController().SetNeedsReload(); |
| contents->GetController().LoadIfNecessary(); |
| contents->Focus(); |
| } |
| } |
| |
| void Discard(DiscardCallback callback) override { |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android, discarding is enabled when kWebContentsDiscard is enabled. |
| if (!base::FeatureList::IsEnabled(features::kWebContentsDiscard)) { |
| return; |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| performance_manager::user_tuning::DiscardAnyPage( |
| mojom::LifecycleUnitDiscardReason::URGENT, |
| /*ignore_minimum_time_in_background=*/true); |
| std::move(callback).Run(); |
| } |
| |
| void ToggleBatterySaverMode() override { |
| performance_manager::user_tuning::prefs::BatterySaverModeState state = |
| performance_manager::user_tuning::prefs:: |
| GetCurrentBatterySaverModeState(g_browser_process->local_state()); |
| g_browser_process->local_state()->SetInteger( |
| performance_manager::user_tuning::prefs::kBatterySaverModeState, |
| static_cast<int>(state == performance_manager::user_tuning::prefs:: |
| BatterySaverModeState::kDisabled |
| ? performance_manager::user_tuning::prefs:: |
| BatterySaverModeState::kEnabled |
| : performance_manager::user_tuning::prefs:: |
| BatterySaverModeState::kDisabled)); |
| } |
| |
| void RefreshPerformanceTabCpuMeasurements() override { |
| #if !BUILDFLAG(IS_DESKTOP_ANDROID) |
| performance_manager::user_tuning::PerformanceDetectionManager::GetInstance() |
| ->ForceTabCpuDataRefresh(); |
| #endif // !BUILDFLAG(IS_DESKTOP_ANDROID) |
| } |
| |
| private: |
| mojo::Receiver<discards::mojom::DetailsProvider> receiver_; |
| |
| // Mapping from id to page node. |
| base::flat_map<int32_t, base::WeakPtr<const PageNode>> page_nodes_by_id_; |
| }; |
| |
| } // namespace |
| |
| DiscardsUI::DiscardsUI(content::WebUI* web_ui) |
| : ui::MojoWebUIController(web_ui) { |
| Profile* profile = Profile::FromWebUI(web_ui); |
| content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd( |
| profile, chrome::kChromeUIDiscardsHost); |
| |
| bool demoModeEnabled = false; |
| #if !BUILDFLAG(IS_DESKTOP_ANDROID) |
| demoModeEnabled = base::FeatureList::IsEnabled( |
| performance_manager::features::kPerformanceInterventionDemoMode); |
| #endif // !BUILDFLAG(IS_DESKTOP_ANDROID) |
| source->AddBoolean("isPerformanceInterventionDemoModeEnabled", |
| demoModeEnabled); |
| |
| webui::SetupWebUIDataSource(source, kDiscardsResources, |
| IDR_DISCARDS_DISCARDS_HTML); |
| |
| content::URLDataSource::Add( |
| profile, std::make_unique<FaviconSource>( |
| profile, chrome::FaviconUrlFormat::kFavicon2)); |
| |
| profile_id_ = profile->UniqueId(); |
| } |
| |
| WEB_UI_CONTROLLER_TYPE_IMPL(DiscardsUI) |
| |
| DiscardsUI::~DiscardsUI() = default; |
| |
| void DiscardsUI::BindInterface( |
| mojo::PendingReceiver<discards::mojom::DetailsProvider> receiver) { |
| performance_manager::PerformanceManager::GetGraph()->PassToGraph( |
| std::make_unique<DiscardsDetailsProviderImpl>(std::move(receiver))); |
| } |
| |
| void DiscardsUI::BindInterface( |
| mojo::PendingReceiver<discards::mojom::SiteDataProvider> receiver) { |
| if (performance_manager::PerformanceManager::IsAvailable()) { |
| // Forward the interface receiver directly to the service. |
| SiteDataProviderImpl::CreateAndBind( |
| std::move(receiver), profile_id_, |
| performance_manager::PerformanceManager::GetGraph()); |
| } |
| } |
| |
| void DiscardsUI::BindInterface( |
| mojo::PendingReceiver<discards::mojom::GraphDump> receiver) { |
| if (performance_manager::PerformanceManager::IsAvailable()) { |
| // Forward the interface receiver directly to the service. |
| DiscardsGraphDumpImpl::CreateAndBind( |
| std::move(receiver), |
| performance_manager::PerformanceManager::GetGraph()); |
| } |
| } |