| // 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/metrics/tab_stats/tab_stats_tracker.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/check_deref.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/function_ref.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/observer_list.h" |
| #include "base/power_monitor/power_monitor.h" |
| #include "base/scoped_multi_source_observation.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/common/buildflags.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/keep_alive_registry/keep_alive_registry.h" |
| #include "components/keep_alive_registry/keep_alive_types.h" |
| #include "components/metrics/daily_event.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/page.h" |
| #include "content/public/browser/visibility.h" |
| #include "content/public/browser/web_contents.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| // We add nognchecks on some includes so that Android bots do not fail |
| // dependency checks. |
| #if BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/android/tab_android.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_list.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_list_observer.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_observer.h" |
| #else |
| #include "chrome/browser/resource_coordinator/lifecycle_unit.h" |
| #include "chrome/browser/resource_coordinator/lifecycle_unit_observer.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h" |
| #include "chrome/browser/resource_coordinator/utils.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_list_observer.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" // nogncheck |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h" // nogncheck |
| #include "chrome/browser/ui/tabs/tab_enums.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" |
| #endif |
| |
| namespace metrics { |
| |
| namespace { |
| |
| // The interval at which the DailyEvent::CheckInterval function should be |
| // called. |
| constexpr base::TimeDelta kDailyEventIntervalTimeDelta = base::Minutes(30); |
| |
| // The interval at which the heartbeat tab metrics should be reported. |
| const base::TimeDelta kTabsHeartbeatReportingInterval = base::Minutes(5); |
| |
| // The global TabStatsTracker instance. |
| TabStatsTracker* g_tab_stats_tracker_instance = nullptr; |
| |
| void UmaHistogramCounts10000WithBatteryStateVariant(const char* histogram_name, |
| size_t value) { |
| auto* power_monitor = base::PowerMonitor::GetInstance(); |
| DCHECK(power_monitor->IsInitialized()); |
| |
| base::UmaHistogramCounts10000(histogram_name, value); |
| |
| const char* suffix = |
| power_monitor->IsOnBatteryPower() ? ".OnBattery" : ".PluggedIn"; |
| |
| base::UmaHistogramCounts10000(base::StrCat({histogram_name, suffix}), value); |
| } |
| |
| } // namespace |
| |
| TabStatsTracker::TabStripInterface::TabStripInterface( |
| TabStripInterface::PlatformModel* model) |
| : model_(model) {} |
| |
| TabStatsTracker::TabStripInterface::~TabStripInterface() = default; |
| |
| void TabStatsTracker::TabStripInterface::ForEachWebContents( |
| base::FunctionRef<void(content::WebContents*)> func) const { |
| for (size_t i = 0; i < GetTabCount(); ++i) { |
| if (auto* web_contents = GetWebContentsAt(i)) { |
| func(web_contents); |
| } |
| } |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| |
| size_t TabStatsTracker::TabStripInterface::GetTabCount() const { |
| return tab_model()->GetTabCount(); |
| } |
| |
| content::WebContents* TabStatsTracker::TabStripInterface::GetActiveWebContents() |
| const { |
| return tab_model()->GetActiveWebContents(); |
| } |
| |
| content::WebContents* TabStatsTracker::TabStripInterface::GetWebContentsAt( |
| size_t index) const { |
| return tab_model()->GetWebContentsAt(index); |
| } |
| |
| Profile* TabStatsTracker::TabStripInterface::GetProfile() const { |
| return tab_model()->GetProfile(); |
| } |
| |
| bool TabStatsTracker::TabStripInterface::IsInNormalBrowser() const { |
| return true; |
| } |
| |
| void TabStatsTracker::TabStripInterface::ActivateTabAtForTesting(size_t index) { |
| tab_model()->SetActiveIndex(index); |
| } |
| |
| void TabStatsTracker::TabStripInterface::CloseTabAtForTesting(size_t index) { |
| tab_model()->CloseTabAt(index); |
| } |
| |
| // static |
| void TabStatsTracker::TabStripInterface::ForEach( |
| base::FunctionRef<void(const TabStripInterface&)> func) { |
| for (TabModel* tab_model : TabModelList::models()) { |
| func(TabStripInterface(tab_model)); |
| } |
| } |
| |
| #else // !BUILDFLAG(IS_ANDROID) |
| |
| size_t TabStatsTracker::TabStripInterface::GetTabCount() const { |
| return browser_window_interface()->GetTabStripModel()->count(); |
| } |
| |
| content::WebContents* TabStatsTracker::TabStripInterface::GetActiveWebContents() |
| const { |
| return browser_window_interface()->GetTabStripModel()->GetActiveWebContents(); |
| } |
| |
| content::WebContents* TabStatsTracker::TabStripInterface::GetWebContentsAt( |
| size_t index) const { |
| return browser_window_interface()->GetTabStripModel()->GetWebContentsAt( |
| index); |
| } |
| |
| Profile* TabStatsTracker::TabStripInterface::GetProfile() const { |
| return const_cast<Profile*>(browser_window_interface()->GetProfile()); |
| } |
| |
| bool TabStatsTracker::TabStripInterface::IsInNormalBrowser() const { |
| return browser_window_interface()->GetType() == |
| BrowserWindowInterface::Type::TYPE_NORMAL; |
| } |
| |
| void TabStatsTracker::TabStripInterface::ActivateTabAtForTesting(size_t index) { |
| browser_window_interface()->GetTabStripModel()->ActivateTabAt(index); |
| } |
| |
| void TabStatsTracker::TabStripInterface::CloseTabAtForTesting(size_t index) { |
| browser_window_interface()->GetTabStripModel()->CloseWebContentsAt( |
| index, TabCloseTypes::CLOSE_USER_GESTURE); |
| } |
| |
| // static |
| void TabStatsTracker::TabStripInterface::ForEach( |
| base::FunctionRef<void(const TabStripInterface&)> func) { |
| ForEachCurrentBrowserWindowInterfaceOrderedByActivation( |
| [&func](BrowserWindowInterface* browser) { |
| func(TabStripInterface(browser)); |
| return true; |
| }); |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| // static |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kNumberOfTabsOnResumeHistogramName[] = "Tabs.NumberOfTabsOnResume"; |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kMaxTabsInADayHistogramName[] = |
| "Tabs.MaxTabsInADay"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kMaxTabsPerWindowInADayHistogramName[] = "Tabs.MaxTabsPerWindowInADay"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kMaxWindowsInADayHistogramName[] = "Tabs.MaxWindowsInADay"; |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kTabCountHistogramName[] = |
| "Tabs.TabCount"; |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kWindowCountHistogramName[] = |
| "Tabs.WindowCount"; |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kWindowWidthHistogramName[] = |
| "Tabs.WindowWidth"; |
| |
| // Daily discard/reload histograms. |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyDiscardsExternalHistogramName[] = "Discarding.DailyDiscards.External"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyDiscardsUrgentHistogramName[] = "Discarding.DailyDiscards.Urgent"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyDiscardsProactiveHistogramName[] = |
| "Discarding.DailyDiscards.Proactive"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyDiscardsSuggestedHistogramName[] = |
| "Discarding.DailyDiscards.Suggested"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyDiscardsFrozenWithGrowingMemoryHistogramName[] = |
| "Discarding.DailyDiscards.FrozenWithGrowingMemory"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyReloadsExternalHistogramName[] = "Discarding.DailyReloads.External"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyReloadsUrgentHistogramName[] = "Discarding.DailyReloads.Urgent"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyReloadsProactiveHistogramName[] = "Discarding.DailyReloads.Proactive"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyReloadsSuggestedHistogramName[] = "Discarding.DailyReloads.Suggested"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kDailyReloadsFrozenWithGrowingMemoryHistogramName[] = |
| "Discarding.DailyReloads.FrozenWithGrowingMemory"; |
| |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicateCountSingleWindowHistogramName[] = |
| "Tabs.Duplicates.Count.SingleWindow"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicateCountAllProfileWindowsHistogramName[] = |
| "Tabs.Duplicates.Count.AllProfileWindows"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicatePercentageSingleWindowHistogramName[] = |
| "Tabs.Duplicates.Percentage.SingleWindow"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicatePercentageAllProfileWindowsHistogramName[] = |
| "Tabs.Duplicates.Percentage.AllProfileWindows"; |
| |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicateExcludingFragmentsCountSingleWindowHistogramName[] = |
| "Tabs.DuplicatesExcludingFragments.Count.SingleWindow"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicateExcludingFragmentsCountAllProfileWindowsHistogramName[] = |
| "Tabs.DuplicatesExcludingFragments.Count.AllProfileWindows"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicateExcludingFragmentsPercentageSingleWindowHistogramName[] = |
| "Tabs.DuplicatesExcludingFragments.Percentage.SingleWindow"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kTabDuplicateExcludingFragmentsPercentageAllProfileWindowsHistogramName[] = |
| "Tabs.DuplicatesExcludingFragments.Percentage.AllProfileWindows"; |
| |
| // When initialized, TabWatcher gets the list of existing windows/tabs. There |
| // shouldn't be any if it's initialized at startup but this will ensure that the |
| // counts stay accurate if the initialization gets moved to after the creation |
| // of the first tab. |
| |
| #if BUILDFLAG(IS_ANDROID) |
| |
| class TabStatsTracker::TabWatcher final : public TabModelListObserver, |
| public TabModelObserver, |
| public TabAndroid::Observer { |
| public: |
| explicit TabWatcher(TabStatsTracker& tracker) : tracker_(tracker) { |
| for (TabModel* tab_model : TabModelList::models()) { |
| OnTabModelAdded(tab_model); |
| for (int i = 0; i < tab_model->GetTabCount(); ++i) { |
| OnTabAdded(tab_model->GetTabAt(i)); |
| } |
| tracker_->OnTabStripNewTabCount(tab_model->GetTabCount()); |
| } |
| TabModelList::AddObserver(this); |
| } |
| |
| ~TabWatcher() final { TabModelList::RemoveObserver(this); } |
| |
| // TabModelListObserver: |
| |
| void OnTabModelAdded(TabModel* tab_model) final { |
| tracker_->OnTabStripAdded(); |
| tab_model_observations_.AddObservation(tab_model); |
| } |
| |
| void OnTabModelRemoved(TabModel* tab_model) final { |
| tab_model_observations_.RemoveObservation(tab_model); |
| tracker_->OnTabStripRemoved(); |
| } |
| |
| // TabModelObserver: |
| |
| void DidAddTab(TabAndroid* tab, TabModel::TabLaunchType type) final { |
| OnTabAdded(tab); |
| auto* tab_model = TabModelList::GetTabModelForTabAndroid(tab); |
| tracker_->OnTabStripNewTabCount(CHECK_DEREF(tab_model).GetTabCount()); |
| } |
| |
| void TabRemoved(TabAndroid* tab) final { |
| // The tab was removed from the model, either because it closed or moved to |
| // a different model. Either way stop watching for the WebContents. |
| if (tab_android_observations_.IsObservingSource(tab)) { |
| tab_android_observations_.RemoveObservation(tab); |
| } |
| } |
| |
| // TabAndroid::Observer: |
| |
| void OnInitWebContents(TabAndroid* tab) final { |
| CHECK(tab->web_contents()); |
| tracker_->OnInitialOrInsertedTab(tab->web_contents()); |
| tab_android_observations_.RemoveObservation(tab); |
| } |
| |
| private: |
| void OnTabAdded(TabAndroid* tab) { |
| if (content::WebContents* web_contents = tab->web_contents()) { |
| tracker_->OnInitialOrInsertedTab(web_contents); |
| } else if (!tab_android_observations_.IsObservingSource(tab)) { |
| // The WebContents hasn't been attached to the tab yet. Start tracking it |
| // when TabAndroid::Observer::OnInitWebContents is called. Note OnTabAdded |
| // can be called while the tab is already being observed, if it's called |
| // from the TabModel constructor while an async DidAddTab notification is |
| // in flight. |
| tab_android_observations_.AddObservation(tab); |
| } |
| } |
| |
| raw_ref<TabStatsTracker> tracker_; |
| base::ScopedMultiSourceObservation<TabModel, TabModelObserver> |
| tab_model_observations_{this}; |
| base::ScopedMultiSourceObservation<TabAndroid, TabAndroid::Observer> |
| tab_android_observations_{this}; |
| }; |
| |
| #else // !BUILDFLAG(IS_ANDROID) |
| |
| class TabStatsTracker::TabWatcher final : public BrowserListObserver, |
| public TabStripModelObserver { |
| public: |
| explicit TabWatcher(TabStatsTracker& tracker) : tracker_(tracker) { |
| ForEachCurrentBrowserWindowInterfaceOrderedByActivation( |
| [this](BrowserWindowInterface* browser) { |
| OnBrowserAdded(browser->GetBrowserForMigrationOnly()); |
| TabStripModel* const tab_strip_model = browser->GetTabStripModel(); |
| for (int i = 0; i < tab_strip_model->count(); ++i) { |
| content::WebContents* const web_contents = |
| tab_strip_model->GetWebContentsAt(i); |
| CHECK(web_contents); |
| tracker_->OnInitialOrInsertedTab(web_contents); |
| } |
| tracker_->OnTabStripNewTabCount(tab_strip_model->count()); |
| return true; |
| }); |
| browser_list_observation_.Observe(BrowserList::GetInstance()); |
| } |
| |
| ~TabWatcher() final = default; |
| |
| // BrowserListObserver: |
| void OnBrowserAdded(Browser* browser) final { |
| tracker_->OnTabStripAdded(); |
| browser->tab_strip_model()->AddObserver(this); |
| } |
| |
| void OnBrowserRemoved(Browser* browser) final { |
| browser->tab_strip_model()->RemoveObserver(this); |
| tracker_->OnTabStripRemoved(); |
| } |
| |
| // TabStripModelObserver: |
| void OnTabStripModelChanged(TabStripModel* tab_strip_model, |
| const TabStripModelChange& change, |
| const TabStripSelectionChange& selection) final { |
| if (change.type() == TabStripModelChange::kInserted) { |
| for (const auto& contents : change.GetInsert()->contents) { |
| tracker_->OnInitialOrInsertedTab(contents.contents); |
| } |
| tracker_->OnTabStripNewTabCount(tab_strip_model->count()); |
| } else if (change.type() == TabStripModelChange::kReplaced) { |
| auto* replace = change.GetReplace(); |
| tracker_->OnTabReplaced(replace->old_contents, replace->new_contents); |
| } |
| } |
| |
| private: |
| raw_ref<TabStatsTracker> tracker_; |
| base::ScopedObservation<BrowserList, BrowserListObserver> |
| browser_list_observation_{this}; |
| }; |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| const TabStatsDataStore::TabsStats& TabStatsTracker::tab_stats() const { |
| return tab_stats_data_store_->tab_stats(); |
| } |
| |
| TabStatsTracker::TabStatsTracker(PrefService* pref_service) |
| : reporting_delegate_(std::make_unique<UmaStatsReportingDelegate>()), |
| tab_stats_data_store_(std::make_unique<TabStatsDataStore>(pref_service)), |
| daily_event_(std::make_unique<DailyEvent>( |
| pref_service, |
| ::prefs::kTabStatsDailySample, |
| // Empty to skip recording the daily event type histogram. |
| /* histogram_name=*/std::string())), |
| tab_watcher_(std::make_unique<TabWatcher>(*this)) { |
| DCHECK(pref_service); |
| |
| AddObserverAndSetInitialState(tab_stats_data_store_.get()); |
| |
| base::PowerMonitor::GetInstance()->AddPowerSuspendObserver(this); |
| |
| // Setup daily reporting of the stats aggregated in |tab_stats_data_store|. |
| daily_event_->AddObserver(std::make_unique<TabStatsDailyObserver>( |
| reporting_delegate_.get(), tab_stats_data_store_.get())); |
| |
| // Call the CheckInterval method to see if the data need to be immediately |
| // reported. |
| daily_event_->CheckInterval(); |
| daily_event_timer_.Start(FROM_HERE, kDailyEventIntervalTimeDelta, |
| daily_event_.get(), &DailyEvent::CheckInterval); |
| |
| heartbeat_timer_.Start(FROM_HERE, kTabsHeartbeatReportingInterval, |
| base::BindRepeating(&TabStatsTracker::OnHeartbeatEvent, |
| base::Unretained(this))); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // TODO(crbug.com/412634171): Enable this when discarding is supported on |
| // Android. |
| resource_coordinator::GetTabLifecycleUnitSource()->AddLifecycleObserver(this); |
| #endif |
| } |
| |
| TabStatsTracker::~TabStatsTracker() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| base::PowerMonitor::GetInstance()->RemovePowerSuspendObserver(this); |
| #if !BUILDFLAG(IS_ANDROID) |
| resource_coordinator::GetTabLifecycleUnitSource()->RemoveLifecycleObserver( |
| this); |
| #endif |
| } |
| |
| // static |
| void TabStatsTracker::SetInstance(std::unique_ptr<TabStatsTracker> instance) { |
| CHECK(!g_tab_stats_tracker_instance); |
| g_tab_stats_tracker_instance = instance.release(); |
| } |
| |
| // static |
| void TabStatsTracker::ClearInstance() { |
| CHECK(g_tab_stats_tracker_instance); |
| delete g_tab_stats_tracker_instance; |
| g_tab_stats_tracker_instance = nullptr; |
| } |
| |
| // static |
| TabStatsTracker* TabStatsTracker::GetInstance() { |
| CHECK(g_tab_stats_tracker_instance); |
| return g_tab_stats_tracker_instance; |
| } |
| |
| // static |
| bool TabStatsTracker::HasInstance() { |
| return g_tab_stats_tracker_instance != nullptr; |
| } |
| |
| void TabStatsTracker::AddObserverAndSetInitialState( |
| TabStatsObserver* observer) { |
| tab_stats_observers_.AddObserver(observer); |
| |
| // Initialization of |this| is complete at this point and all existing |
| // Browsers are already observed. TabStatsObserver functions are called |
| // directly only for |observer| which is new and needs to be caught up to the |
| // current state. |
| TabStripInterface::ForEach([observer](const TabStripInterface& tab_strip) { |
| observer->OnWindowAdded(); |
| tab_strip.ForEachWebContents([observer](content::WebContents* wc) { |
| observer->OnTabAdded(wc); |
| if (wc->GetCurrentlyPlayingVideoCount()) { |
| observer->OnVideoStartedPlaying(wc); |
| } |
| if (wc->IsCurrentlyAudible()) { |
| observer->OnTabIsAudibleChanged(wc); |
| } |
| if (wc->HasActiveEffectivelyFullscreenVideo()) { |
| observer->OnMediaEffectivelyFullscreenChanged(wc, true); |
| } |
| }); |
| }); |
| } |
| |
| void TabStatsTracker::RegisterPrefs(PrefRegistrySimple* registry) { |
| registry->RegisterIntegerPref(::prefs::kTabStatsTotalTabCountMax, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsMaxTabsPerWindow, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsWindowCountMax, 0); |
| DailyEvent::RegisterPref(registry, ::prefs::kTabStatsDailySample); |
| |
| // Preferences for saving discard/reload counts. |
| registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsExternal, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsUrgent, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsProactive, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsSuggested, 0); |
| registry->RegisterIntegerPref( |
| ::prefs::kTabStatsDiscardsFrozenWithGrowingMemory, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsReloadsExternal, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsReloadsUrgent, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsReloadsProactive, 0); |
| registry->RegisterIntegerPref(::prefs::kTabStatsReloadsSuggested, 0); |
| registry->RegisterIntegerPref( |
| ::prefs::kTabStatsReloadsFrozenWithGrowingMemory, 0); |
| } |
| |
| void TabStatsTracker::TabStatsDailyObserver::OnDailyEvent( |
| DailyEvent::IntervalType type) { |
| reporting_delegate_->ReportDailyMetrics(data_store_->tab_stats()); |
| data_store_->ResetMaximumsToCurrentState(); |
| data_store_->ClearTabDiscardAndReloadCounts(); |
| } |
| |
| class TabStatsTracker::WebContentsUsageObserver |
| : public content::WebContentsObserver { |
| public: |
| WebContentsUsageObserver(content::WebContents* web_contents, |
| TabStatsTracker* tab_stats_tracker) |
| : content::WebContentsObserver(web_contents), |
| tab_stats_tracker_(tab_stats_tracker), |
| ukm_source_id_( |
| web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId()), |
| was_playing_video_(web_contents->GetCurrentlyPlayingVideoCount() > 0) {} |
| |
| WebContentsUsageObserver(const WebContentsUsageObserver&) = delete; |
| WebContentsUsageObserver& operator=(const WebContentsUsageObserver&) = delete; |
| |
| // content::WebContentsObserver: |
| void DidStartNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| // Treat browser-initiated navigations as user interactions. |
| if (!navigation_handle->IsRendererInitiated()) { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnTabInteraction(web_contents()); |
| } |
| } |
| // Update navigation time for UKM reporting. |
| navigation_time_ = navigation_handle->NavigationStart(); |
| } |
| |
| void PrimaryPageChanged(content::Page& page) override { |
| ukm_source_id_ = page.GetMainDocument().GetPageUkmSourceId(); |
| |
| // Update observers. |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnPrimaryMainFrameNavigationCommitted(web_contents()); |
| } |
| } |
| |
| void DidGetUserInteraction(const blink::WebInputEvent& event) override { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnTabInteraction(web_contents()); |
| } |
| } |
| |
| void OnVisibilityChanged(content::Visibility visibility) override { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnTabVisibilityChanged(web_contents()); |
| } |
| } |
| |
| void WebContentsDestroyed() override { |
| if (ukm_source_id_) { |
| ukm::builders::TabManager_TabLifetime(ukm_source_id_) |
| .SetTimeSinceNavigation( |
| (base::TimeTicks::Now() - navigation_time_).InMilliseconds()) |
| .Record(ukm::UkmRecorder::Get()); |
| } |
| |
| tab_stats_tracker_->OnWebContentsDestroyed(web_contents()); |
| // The call above will free |this| and so nothing should be done on this |
| // object starting from here. |
| } |
| |
| void OnAudioStateChanged(bool audible) override { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnTabIsAudibleChanged(web_contents()); |
| } |
| } |
| |
| void MediaEffectivelyFullscreenChanged(bool is_fullscreen) override { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnMediaEffectivelyFullscreenChanged(web_contents(), |
| is_fullscreen); |
| } |
| } |
| |
| void MediaStartedPlaying( |
| const content::WebContentsObserver::MediaPlayerInfo& media_type, |
| const content::MediaPlayerId& id) override { |
| MaybeNotifyVideoStartedStoppedPlaying(); |
| } |
| |
| void MediaStoppedPlaying( |
| const content::WebContentsObserver::MediaPlayerInfo& media_type, |
| const content::MediaPlayerId& id, |
| content::WebContentsObserver::MediaStoppedReason reason) override { |
| MaybeNotifyVideoStartedStoppedPlaying(); |
| } |
| |
| void MediaMetadataChanged( |
| const content::WebContentsObserver::MediaPlayerInfo& video_type, |
| const content::MediaPlayerId& id) override { |
| MaybeNotifyVideoStartedStoppedPlaying(); |
| } |
| |
| void MediaDestroyed(const content::MediaPlayerId& id) override { |
| for (auto& tab_stats_observer : tab_stats_tracker_->tab_stats_observers_) |
| tab_stats_observer.OnMediaDestroyed(web_contents()); |
| } |
| |
| void WasDiscarded() override { |
| if (ukm_source_id_) { |
| ukm::builders::TabManager_TabLifetime(ukm_source_id_) |
| .SetTimeSinceNavigation( |
| (base::TimeTicks::Now() - navigation_time_).InMilliseconds()) |
| .Record(ukm::UkmRecorder::Get()); |
| ukm_source_id_ = 0; |
| } |
| |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnTabDiscarded(web_contents()); |
| } |
| } |
| |
| private: |
| void MaybeNotifyVideoStartedStoppedPlaying() { |
| const bool is_playing_video = |
| web_contents()->GetCurrentlyPlayingVideoCount() > 0; |
| |
| if (!was_playing_video_ && is_playing_video) { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnVideoStartedPlaying(web_contents()); |
| } |
| } else if (was_playing_video_ && !is_playing_video) { |
| for (TabStatsObserver& tab_stats_observer : |
| tab_stats_tracker_->tab_stats_observers_) { |
| tab_stats_observer.OnVideoStoppedPlaying(web_contents()); |
| } |
| } |
| |
| was_playing_video_ = is_playing_video; |
| } |
| |
| raw_ptr<TabStatsTracker> tab_stats_tracker_; |
| // The last navigation time associated with this tab. |
| base::TimeTicks navigation_time_ = base::TimeTicks::Now(); |
| // Updated when a navigation is finished. |
| ukm::SourceId ukm_source_id_ = 0; |
| // Whether video was playing in this tab the last time we checked. |
| bool was_playing_video_; |
| }; |
| |
| content::WebContentsObserver* |
| TabStatsTracker::GetWebContentsUsageObserverForTesting( |
| content::WebContents* web_contents) { |
| if (auto it = web_contents_usage_observers_.find(web_contents); |
| it != web_contents_usage_observers_.end()) { |
| return it->second.get(); |
| } |
| return nullptr; |
| } |
| |
| void TabStatsTracker::OnResume() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| reporting_delegate_->ReportTabCountOnResume( |
| tab_stats_data_store_->tab_stats().total_tab_count); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // TODO(crbug.com/412634171): Enable this when discarding is supported on |
| // Android. |
| void TabStatsTracker::OnLifecycleUnitStateChanged( |
| resource_coordinator::LifecycleUnit* lifecycle_unit, |
| ::mojom::LifecycleUnitState previous_state) { |
| const ::mojom::LifecycleUnitState new_state = lifecycle_unit->GetState(); |
| if (previous_state == ::mojom::LifecycleUnitState::DISCARDED || |
| new_state == ::mojom::LifecycleUnitState::DISCARDED) { |
| tab_stats_data_store_->OnTabDiscardStateChange( |
| lifecycle_unit->GetDiscardReason(), |
| new_state == ::mojom::LifecycleUnitState::DISCARDED); |
| } |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| void TabStatsTracker::OnTabStripAdded() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) { |
| tab_stats_observer.OnWindowAdded(); |
| } |
| } |
| |
| void TabStatsTracker::OnTabStripRemoved() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) { |
| tab_stats_observer.OnWindowRemoved(); |
| } |
| } |
| |
| void TabStatsTracker::OnTabStripNewTabCount(size_t new_tab_count) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| tab_stats_data_store_->UpdateMaxTabsPerWindowIfNeeded(new_tab_count); |
| } |
| |
| void TabStatsTracker::OnInitialOrInsertedTab( |
| content::WebContents* web_contents) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // If we already have a WebContentsObserver for this tab then it means that |
| // it's already tracked and it's being dragged into a new window, there's |
| // nothing to do here. |
| if (!base::Contains(web_contents_usage_observers_, web_contents)) { |
| for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) { |
| tab_stats_observer.OnTabAdded(web_contents); |
| } |
| web_contents_usage_observers_.insert(std::make_pair( |
| web_contents, |
| std::make_unique<WebContentsUsageObserver>(web_contents, this))); |
| } |
| } |
| |
| void TabStatsTracker::OnTabReplaced(content::WebContents* old_contents, |
| content::WebContents* new_contents) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) { |
| tab_stats_observer.OnTabReplaced(old_contents, new_contents); |
| } |
| web_contents_usage_observers_.insert(std::make_pair( |
| new_contents, |
| std::make_unique<WebContentsUsageObserver>(new_contents, this))); |
| web_contents_usage_observers_.erase(old_contents); |
| } |
| |
| void TabStatsTracker::OnWebContentsDestroyed( |
| content::WebContents* web_contents) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(base::Contains(web_contents_usage_observers_, web_contents)); |
| web_contents_usage_observers_.erase( |
| web_contents_usage_observers_.find(web_contents)); |
| for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) { |
| tab_stats_observer.OnTabRemoved(web_contents); |
| } |
| } |
| |
| void TabStatsTracker::OnHeartbeatEvent() { |
| reporting_delegate_->ReportHeartbeatMetrics( |
| tab_stats_data_store_->tab_stats()); |
| } |
| |
| void TabStatsTracker::UmaStatsReportingDelegate::ReportTabCountOnResume( |
| size_t tab_count) { |
| // Don't report the number of tabs on resume if Chrome is running in |
| // background with no visible window. |
| if (IsChromeBackgroundedWithoutWindows()) |
| return; |
| UmaHistogramCounts10000WithBatteryStateVariant( |
| kNumberOfTabsOnResumeHistogramName, tab_count); |
| } |
| |
| void TabStatsTracker::UmaStatsReportingDelegate::ReportDailyMetrics( |
| const TabStatsDataStore::TabsStats& tab_stats) { |
| // Don't report the counts if they're equal to 0, this means that Chrome has |
| // only been running in the background since the last time the metrics have |
| // been reported. |
| if (tab_stats.total_tab_count_max == 0) |
| return; |
| UmaHistogramCounts10000WithBatteryStateVariant(kMaxTabsInADayHistogramName, |
| tab_stats.total_tab_count_max); |
| UmaHistogramCounts10000WithBatteryStateVariant( |
| kMaxTabsPerWindowInADayHistogramName, tab_stats.max_tab_per_window); |
| UmaHistogramCounts10000WithBatteryStateVariant(kMaxWindowsInADayHistogramName, |
| tab_stats.window_count_max); |
| |
| // Reports the discard/reload counts. |
| const size_t external_index = |
| static_cast<size_t>(LifecycleUnitDiscardReason::EXTERNAL); |
| const size_t urgent_index = |
| static_cast<size_t>(LifecycleUnitDiscardReason::URGENT); |
| const size_t proactive_index = |
| static_cast<size_t>(LifecycleUnitDiscardReason::PROACTIVE); |
| const size_t suggested_index = |
| static_cast<size_t>(LifecycleUnitDiscardReason::SUGGESTED); |
| const size_t frozen_with_growing_memory_index = static_cast<size_t>( |
| LifecycleUnitDiscardReason::FROZEN_WITH_GROWING_MEMORY); |
| base::UmaHistogramCounts10000(kDailyDiscardsExternalHistogramName, |
| tab_stats.tab_discard_counts[external_index]); |
| base::UmaHistogramCounts10000(kDailyDiscardsUrgentHistogramName, |
| tab_stats.tab_discard_counts[urgent_index]); |
| base::UmaHistogramCounts10000(kDailyDiscardsProactiveHistogramName, |
| tab_stats.tab_discard_counts[proactive_index]); |
| base::UmaHistogramCounts10000(kDailyDiscardsSuggestedHistogramName, |
| tab_stats.tab_discard_counts[suggested_index]); |
| base::UmaHistogramCounts10000( |
| kDailyDiscardsFrozenWithGrowingMemoryHistogramName, |
| tab_stats.tab_discard_counts[frozen_with_growing_memory_index]); |
| base::UmaHistogramCounts10000(kDailyReloadsExternalHistogramName, |
| tab_stats.tab_reload_counts[external_index]); |
| base::UmaHistogramCounts10000(kDailyReloadsUrgentHistogramName, |
| tab_stats.tab_reload_counts[urgent_index]); |
| base::UmaHistogramCounts10000(kDailyReloadsProactiveHistogramName, |
| tab_stats.tab_reload_counts[proactive_index]); |
| base::UmaHistogramCounts10000(kDailyReloadsSuggestedHistogramName, |
| tab_stats.tab_reload_counts[suggested_index]); |
| base::UmaHistogramCounts10000( |
| kDailyReloadsFrozenWithGrowingMemoryHistogramName, |
| tab_stats.tab_reload_counts[frozen_with_growing_memory_index]); |
| } |
| |
| void TabStatsTracker::UmaStatsReportingDelegate::ReportHeartbeatMetrics( |
| const TabStatsDataStore::TabsStats& tab_stats) { |
| // Don't report anything if Chrome is running in background with no visible |
| // window. |
| if (IsChromeBackgroundedWithoutWindows()) |
| return; |
| |
| UmaHistogramCounts10000WithBatteryStateVariant(kTabCountHistogramName, |
| tab_stats.total_tab_count); |
| UmaHistogramCounts10000WithBatteryStateVariant(kWindowCountHistogramName, |
| tab_stats.window_count); |
| if (base::FeatureList::IsEnabled(features::kTabDuplicateMetrics)) { |
| ReportTabDuplicateMetrics(true); |
| ReportTabDuplicateMetrics(false); |
| } |
| #if !BUILDFLAG(IS_ANDROID) |
| // Record the width of all open browser windows with tabs. |
| TabStripInterface::ForEach([&](const TabStripInterface& tab_strip) { |
| if (!tab_strip.IsInNormalBrowser()) { |
| return; |
| } |
| |
| const ui::BaseWindow* window = |
| tab_strip.browser_window_interface()->GetWindow(); |
| |
| // Only consider visible windows. |
| if (!window->IsVisible() || window->IsMinimized()) { |
| return; |
| } |
| |
| // Get the window's size (in DIPs). |
| const gfx::Size window_size = window->GetBounds().size(); |
| |
| // If the size is for some reason 0 in either dimension, skip it. |
| if (window_size.IsEmpty()) { |
| return; |
| } |
| |
| // A 4K screen is 4096 pixels wide. Doubling this and rounding up to |
| // 10000 should give a reasonable upper bound on DIPs. For the |
| // minimum width, pick an arbitrary value of 100. Most screens are |
| // unlikely to be this small, and likewise a browser window's min |
| // width is around this size. |
| UMA_HISTOGRAM_CUSTOM_COUNTS(kWindowWidthHistogramName, window_size.width(), |
| 100, 10000, 50); |
| }); |
| #endif |
| } |
| |
| void TabStatsTracker::UmaStatsReportingDelegate::ReportTabDuplicateMetrics( |
| bool exclude_fragments) { |
| std::map<Profile*, DuplicateData> duplicate_data_per_profile; |
| TabStripInterface::ForEach([&](const TabStripInterface& tab_strip) { |
| if (!tab_strip.IsInNormalBrowser()) { |
| return; |
| } |
| |
| Profile* const profile = tab_strip.GetProfile(); |
| DuplicateData duplicate_data_multi_window = |
| duplicate_data_per_profile[profile]; |
| DuplicateData duplicate_data_single_window = DuplicateData(); |
| |
| const size_t tab_count = tab_strip.GetTabCount(); |
| duplicate_data_multi_window.tab_count += tab_count; |
| duplicate_data_single_window.tab_count = tab_count; |
| |
| tab_strip.ForEachWebContents([&](content::WebContents* web_contents) { |
| const GURL full_url = web_contents->GetURL(); |
| const GURL url = exclude_fragments ? full_url.GetWithoutRef() : full_url; |
| auto seen_urls_single_window_result = |
| duplicate_data_single_window.seen_urls.insert(url); |
| if (!seen_urls_single_window_result.second) { |
| duplicate_data_single_window.duplicate_count++; |
| } |
| // Guest mode and incognito should not count for the per-profile metrics |
| if (profile->IsOffTheRecord()) { |
| return; |
| } |
| auto seen_urls_multi_window_result = |
| duplicate_data_multi_window.seen_urls.insert(url); |
| if (!seen_urls_multi_window_result.second) { |
| duplicate_data_multi_window.duplicate_count++; |
| } |
| }); |
| duplicate_data_per_profile[profile] = duplicate_data_multi_window; |
| |
| base::UmaHistogramCounts100( |
| exclude_fragments |
| ? kTabDuplicateExcludingFragmentsCountSingleWindowHistogramName |
| : kTabDuplicateCountSingleWindowHistogramName, |
| duplicate_data_single_window.duplicate_count); |
| if (duplicate_data_single_window.tab_count > 0) { |
| base::UmaHistogramPercentage( |
| exclude_fragments |
| ? kTabDuplicateExcludingFragmentsPercentageSingleWindowHistogramName |
| : kTabDuplicatePercentageSingleWindowHistogramName, |
| duplicate_data_single_window.duplicate_count * 100 / |
| duplicate_data_single_window.tab_count); |
| } |
| }); |
| |
| for (const auto& duplicate_data : duplicate_data_per_profile) { |
| // Guest mode and incognito should not count for the per-profile metrics |
| Profile* const profile = duplicate_data.first; |
| if (profile->IsOffTheRecord()) { |
| continue; |
| } |
| |
| base::UmaHistogramCounts100( |
| exclude_fragments |
| ? kTabDuplicateExcludingFragmentsCountAllProfileWindowsHistogramName |
| : kTabDuplicateCountAllProfileWindowsHistogramName, |
| duplicate_data.second.duplicate_count); |
| if (duplicate_data.second.tab_count > 0) { |
| base::UmaHistogramPercentage( |
| exclude_fragments |
| ? kTabDuplicateExcludingFragmentsPercentageAllProfileWindowsHistogramName |
| : kTabDuplicatePercentageAllProfileWindowsHistogramName, |
| duplicate_data.second.duplicate_count * 100 / |
| duplicate_data.second.tab_count); |
| } |
| } |
| } |
| |
| bool TabStatsTracker::UmaStatsReportingDelegate:: |
| IsChromeBackgroundedWithoutWindows() { |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| return KeepAliveRegistry::GetInstance()->WouldRestartWithout({ |
| // Transient startup related KeepAlives, not related to any UI. |
| KeepAliveOrigin::SESSION_RESTORE, |
| KeepAliveOrigin::BACKGROUND_MODE_MANAGER_STARTUP, |
| |
| KeepAliveOrigin::BACKGROUND_SYNC, |
| |
| // Notification KeepAlives are not dependent on the Chrome UI being |
| // loaded, and can be registered when we were in pure background mode. |
| // They just block it to avoid issues. Ignore them when determining if we |
| // are in that mode. |
| KeepAliveOrigin::NOTIFICATION, |
| KeepAliveOrigin::PENDING_NOTIFICATION_CLICK_EVENT, |
| KeepAliveOrigin::PENDING_NOTIFICATION_CLOSE_EVENT, |
| KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE, |
| }); |
| #else |
| return false; |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| } |
| |
| TabStatsTracker::UmaStatsReportingDelegate::DuplicateData::DuplicateData() { |
| duplicate_count = 0; |
| tab_count = 0; |
| seen_urls = {}; |
| } |
| |
| TabStatsTracker::UmaStatsReportingDelegate::DuplicateData::DuplicateData( |
| const DuplicateData& other) { |
| duplicate_count = other.duplicate_count; |
| tab_count = other.tab_count; |
| seen_urls = std::set(other.seen_urls); |
| } |
| |
| TabStatsTracker::UmaStatsReportingDelegate::DuplicateData::~DuplicateData() = |
| default; |
| |
| } // namespace metrics |