| // Copyright 2017 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/ui/webui/discards/discards_ui.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/containers/flat_map.h" |
| #include "base/logging.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/engagement/site_engagement_service.h" |
| #include "chrome/browser/performance_manager/public/performance_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/lifecycle_unit.h" |
| #include "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom.h" |
| #include "chrome/browser/resource_coordinator/local_site_characteristics_data_reader.h" |
| #include "chrome/browser/resource_coordinator/local_site_characteristics_data_store.h" |
| #include "chrome/browser/resource_coordinator/local_site_characteristics_data_store_inspector.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/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/favicon_source.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/grit/browser_resources.h" |
| #include "components/favicon_base/favicon_url_parser.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 "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace { |
| |
| mojom::LifecycleUnitDiscardReason GetDiscardReason(bool urgent) { |
| return urgent ? mojom::LifecycleUnitDiscardReason::URGENT |
| : mojom::LifecycleUnitDiscardReason::PROACTIVE; |
| } |
| |
| 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(); |
| return discards::mojom::LifecycleUnitVisibility::VISIBLE; |
| #endif |
| } |
| |
| resource_coordinator::LifecycleUnit* GetLifecycleUnitById(int32_t id) { |
| for (resource_coordinator::LifecycleUnit* lifecycle_unit : |
| g_browser_process->GetTabManager()->GetSortedLifecycleUnits()) { |
| if (lifecycle_unit->GetID() == id) |
| return lifecycle_unit; |
| } |
| return nullptr; |
| } |
| |
| 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 = SiteEngagementService::Get( |
| Profile::FromBrowserContext(contents->GetBrowserContext())); |
| return engagement_svc->GetDetails(nav_entry->GetURL()).total_score; |
| } |
| |
| discards::mojom::SiteCharacteristicsFeaturePtr ConvertFeatureFromProto( |
| const SiteDataFeatureProto& proto) { |
| discards::mojom::SiteCharacteristicsFeaturePtr feature = |
| discards::mojom::SiteCharacteristicsFeature::New(); |
| |
| if (proto.has_observation_duration()) { |
| feature->observation_duration = proto.observation_duration(); |
| } else { |
| feature->observation_duration = 0; |
| } |
| |
| if (proto.has_use_timestamp()) { |
| feature->use_timestamp = proto.use_timestamp(); |
| } else { |
| feature->use_timestamp = 0; |
| } |
| |
| return feature; |
| } |
| |
| discards::mojom::SiteCharacteristicsDatabaseEntryPtr ConvertEntryFromProto( |
| SiteDataProto* proto) { |
| discards::mojom::SiteCharacteristicsDatabaseValuePtr value = |
| discards::mojom::SiteCharacteristicsDatabaseValue::New(); |
| |
| if (proto->has_last_loaded()) { |
| value->last_loaded = proto->last_loaded(); |
| } else { |
| value->last_loaded = 0; |
| } |
| value->updates_favicon_in_background = |
| ConvertFeatureFromProto(proto->updates_favicon_in_background()); |
| value->updates_title_in_background = |
| ConvertFeatureFromProto(proto->updates_title_in_background()); |
| value->uses_audio_in_background = |
| ConvertFeatureFromProto(proto->uses_audio_in_background()); |
| value->uses_notifications_in_background = |
| ConvertFeatureFromProto(proto->uses_notifications_in_background()); |
| |
| if (proto->has_load_time_estimates()) { |
| const auto& load_time_estimates_proto = proto->load_time_estimates(); |
| DCHECK(load_time_estimates_proto.has_avg_cpu_usage_us()); |
| DCHECK(load_time_estimates_proto.has_avg_footprint_kb()); |
| |
| discards::mojom::SiteCharacteristicsPerformanceMeasurementPtr |
| load_time_estimates = |
| discards::mojom::SiteCharacteristicsPerformanceMeasurement::New(); |
| if (load_time_estimates_proto.has_avg_cpu_usage_us()) { |
| load_time_estimates->avg_cpu_usage_us = |
| load_time_estimates_proto.avg_cpu_usage_us(); |
| } |
| if (load_time_estimates_proto.has_avg_footprint_kb()) { |
| load_time_estimates->avg_footprint_kb = |
| load_time_estimates_proto.avg_footprint_kb(); |
| } |
| if (load_time_estimates_proto.has_avg_load_duration_us()) { |
| load_time_estimates->avg_load_duration_us = |
| load_time_estimates_proto.avg_load_duration_us(); |
| } |
| |
| value->load_time_estimates = std::move(load_time_estimates); |
| } |
| |
| discards::mojom::SiteCharacteristicsDatabaseEntryPtr entry = |
| discards::mojom::SiteCharacteristicsDatabaseEntry::New(); |
| entry->value = std::move(value); |
| return entry; |
| } |
| |
| class DiscardsDetailsProviderImpl : public discards::mojom::DetailsProvider { |
| public: |
| // This instance is deleted when the supplied pipe is destroyed. |
| DiscardsDetailsProviderImpl( |
| resource_coordinator::LocalSiteCharacteristicsDataStoreInspector* |
| data_store_inspector, |
| mojo::PendingReceiver<discards::mojom::DetailsProvider> receiver) |
| : data_store_inspector_(data_store_inspector), |
| receiver_(this, std::move(receiver)) {} |
| |
| ~DiscardsDetailsProviderImpl() override {} |
| |
| // discards::mojom::DetailsProvider overrides: |
| void GetTabDiscardsInfo(GetTabDiscardsInfoCallback callback) override { |
| resource_coordinator::TabManager* tab_manager = |
| g_browser_process->GetTabManager(); |
| const resource_coordinator::LifecycleUnitVector lifecycle_units = |
| tab_manager->GetSortedLifecycleUnits(); |
| |
| std::vector<discards::mojom::TabDiscardsInfoPtr> infos; |
| infos.reserve(lifecycle_units.size()); |
| |
| const base::TimeTicks now = resource_coordinator::NowTicks(); |
| |
| // Convert the LifecycleUnits to a vector of TabDiscardsInfos. |
| size_t rank = 1; |
| for (auto* lifecycle_unit : lifecycle_units) { |
| discards::mojom::TabDiscardsInfoPtr info( |
| discards::mojom::TabDiscardsInfo::New()); |
| |
| resource_coordinator::TabLifecycleUnitExternal* |
| tab_lifecycle_unit_external = |
| lifecycle_unit->AsTabLifecycleUnitExternal(); |
| content::WebContents* contents = |
| tab_lifecycle_unit_external->GetWebContents(); |
| |
| info->tab_url = contents->GetLastCommittedURL().spec(); |
| info->title = base::UTF16ToUTF8(lifecycle_unit->GetTitle()); |
| info->visibility = |
| GetLifecycleUnitVisibility(lifecycle_unit->GetVisibility()); |
| info->loading_state = lifecycle_unit->GetLoadingState(); |
| info->state = lifecycle_unit->GetState(); |
| resource_coordinator::DecisionDetails freeze_details; |
| info->can_freeze = lifecycle_unit->CanFreeze(&freeze_details); |
| info->cannot_freeze_reasons = freeze_details.GetFailureReasonStrings(); |
| resource_coordinator::DecisionDetails discard_details; |
| info->can_discard = lifecycle_unit->CanDiscard( |
| mojom::LifecycleUnitDiscardReason::PROACTIVE, &discard_details); |
| info->cannot_discard_reasons = discard_details.GetFailureReasonStrings(); |
| info->discard_count = lifecycle_unit->GetDiscardCount(); |
| // This is only valid if the state is PENDING_DISCARD or DISCARD, but the |
| // javascript code takes care of that. |
| info->discard_reason = lifecycle_unit->GetDiscardReason(); |
| info->utility_rank = rank++; |
| const base::TimeTicks last_focused_time = |
| lifecycle_unit->GetLastFocusedTime(); |
| const base::TimeDelta elapsed = |
| (last_focused_time == base::TimeTicks::Max()) |
| ? base::TimeDelta() |
| : (now - last_focused_time); |
| info->last_active_seconds = static_cast<int32_t>(elapsed.InSeconds()); |
| info->is_auto_discardable = |
| tab_lifecycle_unit_external->IsAutoDiscardable(); |
| info->id = lifecycle_unit->GetID(); |
| base::Optional<float> reactivation_score = |
| resource_coordinator::TabActivityWatcher::GetInstance() |
| ->CalculateReactivationScore(contents); |
| info->has_reactivation_score = reactivation_score.has_value(); |
| if (info->has_reactivation_score) |
| info->reactivation_score = reactivation_score.value(); |
| info->site_engagement_score = GetSiteEngagementScore(contents); |
| info->state_change_time = |
| lifecycle_unit->GetStateChangeTime() - base::TimeTicks::UnixEpoch(); |
| // TODO(crbug.com/876340): The focus is used to compute the page lifecycle |
| // state. This should be replaced with the actual page lifecycle state |
| // information from Blink, but this depends on implementing the passive |
| // state and plumbing it to the browser. |
| info->has_focus = lifecycle_unit->GetLastFocusedTime().is_max(); |
| |
| infos.push_back(std::move(info)); |
| } |
| |
| std::move(callback).Run(std::move(infos)); |
| } |
| void GetSiteCharacteristicsDatabase( |
| const std::vector<std::string>& explicitly_requested_origins, |
| GetSiteCharacteristicsDatabaseCallback callback) override; |
| void GetSiteCharacteristicsDatabaseSize( |
| GetSiteCharacteristicsDatabaseSizeCallback callback) override; |
| |
| void SetAutoDiscardable(int32_t id, |
| bool is_auto_discardable, |
| SetAutoDiscardableCallback callback) override { |
| auto* lifecycle_unit = GetLifecycleUnitById(id); |
| if (lifecycle_unit) { |
| auto* tab_lifecycle_unit_external = |
| lifecycle_unit->AsTabLifecycleUnitExternal(); |
| if (tab_lifecycle_unit_external) |
| tab_lifecycle_unit_external->SetAutoDiscardable(is_auto_discardable); |
| } |
| std::move(callback).Run(); |
| } |
| |
| void DiscardById(int32_t id, |
| bool urgent, |
| DiscardByIdCallback callback) override { |
| auto* lifecycle_unit = GetLifecycleUnitById(id); |
| if (lifecycle_unit) |
| lifecycle_unit->Discard(GetDiscardReason(urgent)); |
| std::move(callback).Run(); |
| } |
| |
| void FreezeById(int32_t id) override { |
| auto* lifecycle_unit = GetLifecycleUnitById(id); |
| if (lifecycle_unit) |
| lifecycle_unit->Freeze(); |
| } |
| |
| void LoadById(int32_t id) override { |
| auto* lifecycle_unit = GetLifecycleUnitById(id); |
| if (lifecycle_unit) |
| lifecycle_unit->Load(); |
| } |
| |
| void Discard(bool urgent, DiscardCallback callback) override { |
| resource_coordinator::TabManager* tab_manager = |
| g_browser_process->GetTabManager(); |
| tab_manager->DiscardTab(GetDiscardReason(urgent)); |
| std::move(callback).Run(); |
| } |
| |
| private: |
| using LocalSiteCharacteristicsDataStoreInspector = |
| resource_coordinator::LocalSiteCharacteristicsDataStoreInspector; |
| using SiteCharacteristicsDataReader = |
| resource_coordinator::SiteCharacteristicsDataReader; |
| using SiteCharacteristicsDataStore = |
| resource_coordinator::SiteCharacteristicsDataStore; |
| using OriginToReaderMap = |
| base::flat_map<std::string, |
| std::unique_ptr<SiteCharacteristicsDataReader>>; |
| |
| // This map pins requested readers and their associated data in memory until |
| // after the next read finishes. This is necessary to allow the database reads |
| // to go through and populate the requested entries. |
| OriginToReaderMap requested_origins_; |
| |
| LocalSiteCharacteristicsDataStoreInspector* data_store_inspector_; |
| mojo::Receiver<discards::mojom::DetailsProvider> receiver_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DiscardsDetailsProviderImpl); |
| }; |
| |
| void DiscardsDetailsProviderImpl::GetSiteCharacteristicsDatabase( |
| const std::vector<std::string>& explicitly_requested_origins, |
| GetSiteCharacteristicsDatabaseCallback callback) { |
| if (!data_store_inspector_) { |
| // Early return with a nullptr if there's no inspector. |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| |
| // Move all previously explicitly requested origins to this local map. |
| // Move any currently requested origins over to the member variable, or |
| // populate them if they weren't previously requested. |
| // The difference will remain in this map and go out of scope at the end of |
| // this function. |
| OriginToReaderMap prev_requested_origins; |
| prev_requested_origins.swap(requested_origins_); |
| SiteCharacteristicsDataStore* data_store = |
| data_store_inspector_->GetDataStore(); |
| DCHECK(data_store); |
| for (const std::string& origin : explicitly_requested_origins) { |
| auto it = prev_requested_origins.find(origin); |
| if (it == prev_requested_origins.end()) { |
| GURL url(origin); |
| requested_origins_[origin] = |
| data_store->GetReaderForOrigin(url::Origin::Create(url)); |
| } else { |
| requested_origins_[origin] = std::move(it->second); |
| prev_requested_origins.erase(it); |
| } |
| } |
| |
| discards::mojom::SiteCharacteristicsDatabasePtr result = |
| discards::mojom::SiteCharacteristicsDatabase::New(); |
| std::vector<url::Origin> in_memory_origins = |
| data_store_inspector_->GetAllInMemoryOrigins(); |
| for (const url::Origin& origin : in_memory_origins) { |
| // Get the data for this origin and convert it from proto to the |
| // corresponding mojo structure. |
| std::unique_ptr<SiteDataProto> proto; |
| bool is_dirty = false; |
| if (data_store_inspector_->GetDataForOrigin(origin, &is_dirty, &proto)) { |
| auto entry = ConvertEntryFromProto(proto.get()); |
| entry->origin = origin.Serialize(); |
| entry->is_dirty = is_dirty; |
| result->db_rows.push_back(std::move(entry)); |
| } |
| } |
| |
| // Return the result. |
| std::move(callback).Run(std::move(result)); |
| } |
| |
| void DiscardsDetailsProviderImpl::GetSiteCharacteristicsDatabaseSize( |
| GetSiteCharacteristicsDatabaseSizeCallback callback) { |
| if (!data_store_inspector_) { |
| // Early return with a nullptr if there's no inspector. |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| |
| // Adapt the inspector callback to the mojom callback with this lambda. |
| auto inspector_callback = base::BindOnce( |
| [](GetSiteCharacteristicsDatabaseSizeCallback callback, |
| base::Optional<int64_t> num_rows, |
| base::Optional<int64_t> on_disk_size_kb) { |
| discards::mojom::SiteCharacteristicsDatabaseSizePtr result = |
| discards::mojom::SiteCharacteristicsDatabaseSize::New(); |
| result->num_rows = num_rows.has_value() ? num_rows.value() : -1; |
| result->on_disk_size_kb = |
| on_disk_size_kb.has_value() ? on_disk_size_kb.value() : -1; |
| |
| std::move(callback).Run(std::move(result)); |
| }, |
| std::move(callback)); |
| |
| data_store_inspector_->GetDatabaseSize(std::move(inspector_callback)); |
| } |
| |
| } // namespace |
| |
| DiscardsUI::DiscardsUI(content::WebUI* web_ui) |
| : ui::MojoWebUIController(web_ui) { |
| std::unique_ptr<content::WebUIDataSource> source( |
| content::WebUIDataSource::Create(chrome::kChromeUIDiscardsHost)); |
| |
| source->AddResourcePath("discards.js", IDR_DISCARDS_JS); |
| |
| source->AddResourcePath("discards_main.html", |
| IDR_DISCARDS_DISCARDS_MAIN_HTML); |
| source->AddResourcePath("discards_main.js", IDR_DISCARDS_DISCARDS_MAIN_JS); |
| |
| source->AddResourcePath("database_tab.html", IDR_DISCARDS_DATABASE_TAB_HTML); |
| source->AddResourcePath("database_tab.js", IDR_DISCARDS_DATABASE_TAB_JS); |
| source->AddResourcePath("discards_tab.html", IDR_DISCARDS_DISCARDS_TAB_HTML); |
| source->AddResourcePath("discards_tab.js", IDR_DISCARDS_DISCARDS_TAB_JS); |
| source->AddResourcePath("sorted_table_behavior.html", |
| IDR_DISCARDS_SORTED_TABLE_BEHAVIOR_HTML); |
| source->AddResourcePath("sorted_table_behavior.js", |
| IDR_DISCARDS_SORTED_TABLE_BEHAVIOR_JS); |
| |
| source->AddResourcePath("graph_tab.html", IDR_DISCARDS_GRAPH_TAB_HTML); |
| source->AddResourcePath("graph_tab.js", IDR_DISCARDS_GRAPH_TAB_JS); |
| |
| source->AddResourcePath("mojo_api.html", IDR_DISCARDS_MOJO_API_HTML); |
| |
| // Full paths (relative to src) are important for Mojom generated files. |
| source->AddResourcePath( |
| "chrome/browser/ui/webui/discards/discards.mojom-lite.js", |
| IDR_DISCARDS_MOJOM_LITE_JS); |
| source->AddResourcePath( |
| "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom-lite.js", |
| IDR_DISCARDS_LIFECYCLE_UNIT_STATE_MOJOM_LITE_JS); |
| |
| // Add the mojo base dependency for the WebUI Graph Dump. |
| source->AddResourcePath( |
| "mojo/public/mojom/base/process_id.mojom-lite.js", |
| IDR_DISCARDS_MOJO_PUBLIC_BASE_PROCESS_ID_MOJOM_LITE_JS); |
| |
| source->SetDefaultResource(IDR_DISCARDS_HTML); |
| |
| Profile* profile = Profile::FromWebUI(web_ui); |
| content::WebUIDataSource::Add(profile, source.release()); |
| |
| content::URLDataSource::Add( |
| profile, std::make_unique<FaviconSource>( |
| profile, chrome::FaviconUrlFormat::kFavicon2)); |
| |
| AddHandlerToRegistry(base::BindRepeating( |
| &DiscardsUI::BindDiscardsDetailsProvider, base::Unretained(this))); |
| AddHandlerToRegistry(base::BindRepeating( |
| &DiscardsUI::BindDiscardsGraphDumpProvider, base::Unretained(this))); |
| |
| data_store_inspector_ = resource_coordinator:: |
| LocalSiteCharacteristicsDataStoreInspector::GetForProfile(profile); |
| } |
| |
| DiscardsUI::~DiscardsUI() {} |
| |
| void DiscardsUI::BindDiscardsDetailsProvider( |
| mojo::PendingReceiver<discards::mojom::DetailsProvider> receiver) { |
| ui_handler_ = std::make_unique<DiscardsDetailsProviderImpl>( |
| data_store_inspector_, std::move(receiver)); |
| } |
| |
| void DiscardsUI::BindDiscardsGraphDumpProvider( |
| mojo::PendingReceiver<discards::mojom::GraphDump> receiver) { |
| if (performance_manager::PerformanceManager::IsAvailable()) { |
| // Forward the interface receiver directly to the service. |
| performance_manager::PerformanceManager::CallOnGraph( |
| FROM_HERE, base::BindOnce(&DiscardsGraphDumpImpl::CreateAndBind, |
| std::move(receiver))); |
| } |
| } |