| // 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/metrics/tab_stats_tracker.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/power_monitor/power_monitor.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/browser_process.h" |
| #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_external.h" |
| #include "chrome/browser/resource_coordinator/tab_manager.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/buildflags.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/metrics/daily_event.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/ukm/content/source_url_recorder.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" |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| #include "chrome/browser/background/background_mode_manager.h" |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| namespace metrics { |
| |
| namespace { |
| |
| // The interval at which the DailyEvent::CheckInterval function should be |
| // called. |
| constexpr base::TimeDelta kDailyEventIntervalTimeDelta = |
| base::TimeDelta::FromMinutes(30); |
| |
| // The intervals at which we report the number of unused tabs. This is used for |
| // all the tab usage histograms listed below. |
| // |
| // The 'Tabs.TabUsageIntervalLength' histogram suffixes entry in histograms.xml |
| // should be kept in sync with these values. |
| constexpr base::TimeDelta kTabUsageReportingIntervals[] = { |
| base::TimeDelta::FromSeconds(30), base::TimeDelta::FromMinutes(1), |
| base::TimeDelta::FromMinutes(10), base::TimeDelta::FromHours(1), |
| base::TimeDelta::FromHours(5), base::TimeDelta::FromHours(12)}; |
| |
| #if defined(OS_WIN) |
| const base::TimeDelta kNativeWindowOcclusionCalculationInterval = |
| base::TimeDelta::FromMinutes(10); |
| #endif |
| |
| // The interval at which the heartbeat tab metrics should be reported. |
| const base::TimeDelta kTabsHeartbeatReportingInterval = |
| base::TimeDelta::FromMinutes(5); |
| |
| // The global TabStatsTracker instance. |
| TabStatsTracker* g_tab_stats_tracker_instance = nullptr; |
| |
| // Ensure that an interval is a valid one (i.e. listed in |
| // |kTabUsageReportingIntervals|). |
| bool IsValidInterval(base::TimeDelta interval) { |
| return base::Contains(kTabUsageReportingIntervals, interval); |
| } |
| |
| } // namespace |
| |
| // static |
| const char TabStatsTracker::kTabStatsDailyEventHistogramName[] = |
| "Tabs.TabsStatsDailyEventInterval"; |
| 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"; |
| |
| // Tab usage histograms. |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kUnusedAndClosedInIntervalHistogramNameBase[] = |
| "Tabs.UnusedAndClosedInInterval.Count"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kUnusedTabsInIntervalHistogramNameBase[] = "Tabs.UnusedInInterval.Count"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kUsedAndClosedInIntervalHistogramNameBase[] = |
| "Tabs.UsedAndClosedInInterval.Count"; |
| const char TabStatsTracker::UmaStatsReportingDelegate:: |
| kUsedTabsInIntervalHistogramNameBase[] = "Tabs.UsedInInterval.Count"; |
| |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kTabCountHistogramName[] = |
| "Tabs.TabCount"; |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kWindowCountHistogramName[] = |
| "Tabs.WindowCount"; |
| |
| const char |
| TabStatsTracker::UmaStatsReportingDelegate::kWindowWidthHistogramName[] = |
| "Tabs.WindowWidth"; |
| |
| 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>()), |
| delegate_(std::make_unique<TabStatsTrackerDelegate>()), |
| tab_stats_data_store_(std::make_unique<TabStatsDataStore>(pref_service)), |
| daily_event_( |
| std::make_unique<DailyEvent>(pref_service, |
| ::prefs::kTabStatsDailySample, |
| kTabStatsDailyEventHistogramName)) { |
| DCHECK(pref_service); |
| // Get the list of existing windows/tabs. There shouldn't be any if this is |
| // 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. |
| BrowserList* browser_list = BrowserList::GetInstance(); |
| for (Browser* browser : *browser_list) { |
| OnBrowserAdded(browser); |
| for (int i = 0; i < browser->tab_strip_model()->count(); ++i) |
| OnInitialOrInsertedTab(browser->tab_strip_model()->GetWebContentsAt(i)); |
| tab_stats_data_store_->UpdateMaxTabsPerWindowIfNeeded( |
| static_cast<size_t>(browser->tab_strip_model()->count())); |
| } |
| |
| browser_list->AddObserver(this); |
| base::PowerMonitor::AddObserver(this); |
| |
| 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); |
| |
| // Initialize the interval maps and timers associated with them. |
| for (base::TimeDelta interval : kTabUsageReportingIntervals) { |
| TabStatsDataStore::TabsStateDuringIntervalMap* interval_map = |
| tab_stats_data_store_->AddInterval(); |
| // Setup the timer associated with this interval. |
| std::unique_ptr<base::RepeatingTimer> timer = |
| std::make_unique<base::RepeatingTimer>(); |
| timer->Start( |
| FROM_HERE, interval, |
| base::BindRepeating(&TabStatsTracker::OnInterval, |
| base::Unretained(this), interval, interval_map)); |
| usage_interval_timers_.push_back(std::move(timer)); |
| } |
| |
| // The native window occlusion calculation is specific to Windows. |
| #if defined(OS_WIN) |
| native_window_occlusion_timer_.Start( |
| FROM_HERE, kNativeWindowOcclusionCalculationInterval, |
| base::BindRepeating( |
| &TabStatsTracker::CalculateAndRecordNativeWindowVisibilities, |
| base::Unretained(this))); |
| #endif |
| |
| heartbeat_timer_.Start(FROM_HERE, kTabsHeartbeatReportingInterval, |
| base::BindRepeating(&TabStatsTracker::OnHeartbeatEvent, |
| base::Unretained(this))); |
| } |
| |
| TabStatsTracker::~TabStatsTracker() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| BrowserList::GetInstance()->RemoveObserver(this); |
| base::PowerMonitor::RemoveObserver(this); |
| } |
| |
| // static |
| void TabStatsTracker::SetInstance(std::unique_ptr<TabStatsTracker> instance) { |
| DCHECK_EQ(nullptr, g_tab_stats_tracker_instance); |
| g_tab_stats_tracker_instance = instance.release(); |
| } |
| |
| TabStatsTracker* TabStatsTracker::GetInstance() { |
| return g_tab_stats_tracker_instance; |
| } |
| |
| 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); |
| } |
| |
| void TabStatsTracker::SetDelegateForTesting( |
| std::unique_ptr<TabStatsTrackerDelegate> new_delegate) { |
| delegate_ = std::move(new_delegate); |
| } |
| |
| void TabStatsTracker::TabStatsDailyObserver::OnDailyEvent( |
| DailyEvent::IntervalType type) { |
| reporting_delegate_->ReportDailyMetrics(data_store_->tab_stats()); |
| data_store_->ResetMaximumsToCurrentState(); |
| } |
| |
| 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_(ukm::GetSourceIdForWebContentsDocument(web_contents)) {} |
| |
| // content::WebContentsObserver: |
| void DidStartNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| // Treat browser-initiated navigations as user interactions. |
| if (!navigation_handle->IsRendererInitiated()) { |
| tab_stats_tracker_->tab_stats_data_store()->OnTabInteraction( |
| web_contents()); |
| } |
| } |
| |
| void DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| if (!navigation_handle->HasCommitted() || |
| !navigation_handle->IsInMainFrame() || |
| navigation_handle->IsSameDocument()) { |
| return; |
| } |
| // Update navigation time for UKM reporting. |
| navigation_time_ = navigation_handle->NavigationStart(); |
| ukm_source_id_ = ukm::ConvertToSourceId( |
| navigation_handle->GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID); |
| } |
| |
| void DidGetUserInteraction(const blink::WebInputEvent& event) override { |
| tab_stats_tracker_->tab_stats_data_store()->OnTabInteraction( |
| web_contents()); |
| } |
| |
| void OnVisibilityChanged(content::Visibility visibility) override { |
| if (visibility == content::Visibility::VISIBLE) |
| tab_stats_tracker_->tab_stats_data_store()->OnTabVisible(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. |
| } |
| |
| private: |
| 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; |
| |
| DISALLOW_COPY_AND_ASSIGN(WebContentsUsageObserver); |
| }; |
| |
| void TabStatsTracker::OnBrowserAdded(Browser* browser) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| tab_stats_data_store_->OnWindowAdded(); |
| browser->tab_strip_model()->AddObserver(this); |
| } |
| |
| void TabStatsTracker::OnBrowserRemoved(Browser* browser) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| tab_stats_data_store_->OnWindowRemoved(); |
| browser->tab_strip_model()->RemoveObserver(this); |
| } |
| |
| void TabStatsTracker::OnTabStripModelChanged( |
| TabStripModel* tab_strip_model, |
| const TabStripModelChange& change, |
| const TabStripSelectionChange& selection) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (change.type() == TabStripModelChange::kInserted) { |
| for (const auto& contents : change.GetInsert()->contents) |
| OnInitialOrInsertedTab(contents.contents); |
| |
| tab_stats_data_store_->UpdateMaxTabsPerWindowIfNeeded( |
| static_cast<size_t>(tab_strip_model->count())); |
| |
| return; |
| } |
| |
| if (change.type() == TabStripModelChange::kReplaced) { |
| auto* replace = change.GetReplace(); |
| tab_stats_data_store_->OnTabReplaced(replace->old_contents, |
| replace->new_contents); |
| web_contents_usage_observers_.insert(std::make_pair( |
| replace->new_contents, std::make_unique<WebContentsUsageObserver>( |
| replace->new_contents, this))); |
| web_contents_usage_observers_.erase(replace->old_contents); |
| } |
| } |
| |
| void TabStatsTracker::TabChangedAt(content::WebContents* web_contents, |
| int index, |
| TabChangeType change_type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Ignore 'loading' and 'title' changes, we're only interested in audio here. |
| if (change_type != TabChangeType::kAll) |
| return; |
| if (web_contents->IsCurrentlyAudible()) |
| tab_stats_data_store_->OnTabAudible(web_contents); |
| } |
| |
| void TabStatsTracker::OnResume() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| reporting_delegate_->ReportTabCountOnResume( |
| tab_stats_data_store_->tab_stats().total_tab_count); |
| } |
| |
| void TabStatsTracker::OnInterval( |
| base::TimeDelta interval, |
| TabStatsDataStore::TabsStateDuringIntervalMap* interval_map) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(interval_map); |
| reporting_delegate_->ReportUsageDuringInterval(*interval_map, interval); |
| // Reset the interval data. |
| tab_stats_data_store_->ResetIntervalData(interval_map); |
| } |
| |
| 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)) { |
| tab_stats_data_store_->OnTabAdded(web_contents); |
| web_contents_usage_observers_.insert(std::make_pair( |
| web_contents, |
| std::make_unique<WebContentsUsageObserver>(web_contents, this))); |
| } |
| } |
| |
| 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)); |
| tab_stats_data_store_->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; |
| UMA_HISTOGRAM_COUNTS_10000(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; |
| UMA_HISTOGRAM_COUNTS_10000(kMaxTabsInADayHistogramName, |
| tab_stats.total_tab_count_max); |
| UMA_HISTOGRAM_COUNTS_10000(kMaxTabsPerWindowInADayHistogramName, |
| tab_stats.max_tab_per_window); |
| UMA_HISTOGRAM_COUNTS_10000(kMaxWindowsInADayHistogramName, |
| tab_stats.window_count_max); |
| } |
| |
| 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; |
| |
| UMA_HISTOGRAM_COUNTS_10000(kTabCountHistogramName, tab_stats.total_tab_count); |
| UMA_HISTOGRAM_COUNTS_10000(kWindowCountHistogramName, tab_stats.window_count); |
| |
| // Record the width of all open browser windows with tabs. |
| for (Browser* browser : *BrowserList::GetInstance()) { |
| if (browser->type() != Browser::TYPE_NORMAL) |
| continue; |
| |
| const BrowserWindow* window = browser->window(); |
| |
| // Only consider visible windows. |
| if (!window->IsVisible() || window->IsMinimized()) |
| continue; |
| |
| // Get the window's size (in DIPs). |
| const gfx::Size window_size = browser->window()->GetBounds().size(); |
| |
| // If the size is for some reason 0 in either dimension, skip it. |
| if (window_size.IsEmpty()) |
| continue; |
| |
| // 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); |
| } |
| } |
| |
| void TabStatsTracker::UmaStatsReportingDelegate::ReportUsageDuringInterval( |
| const TabStatsDataStore::TabsStateDuringIntervalMap& interval_map, |
| base::TimeDelta interval) { |
| // Counts the number of used/unused tabs during this interval, a tabs counts |
| // as unused if it hasn't been interacted with or visible during the duration |
| // of the interval. |
| size_t used_tabs = 0; |
| size_t used_and_closed_tabs = 0; |
| size_t unused_tabs = 0; |
| size_t unused_and_closed_tabs = 0; |
| for (const auto& iter : interval_map) { |
| // There's currently no distinction between a visible/audible tab and one |
| // that has been interacted with in these metrics. |
| // TODO(sebmarchand): Add a metric that track the number of tab that have |
| // been visible/audible but not interacted with during an interval, |
| // https://crbug.com/800828. |
| if (iter.second.interacted_during_interval || |
| iter.second.visible_or_audible_during_interval) { |
| if (iter.second.exists_currently) |
| ++used_tabs; |
| else |
| ++used_and_closed_tabs; |
| } else { |
| if (iter.second.exists_currently) |
| ++unused_tabs; |
| else |
| ++unused_and_closed_tabs; |
| } |
| } |
| |
| std::string used_and_closed_histogram_name = GetIntervalHistogramName( |
| UmaStatsReportingDelegate::kUsedAndClosedInIntervalHistogramNameBase, |
| interval); |
| std::string used_histogram_name = GetIntervalHistogramName( |
| UmaStatsReportingDelegate::kUsedTabsInIntervalHistogramNameBase, |
| interval); |
| std::string unused_and_closed_histogram_name = GetIntervalHistogramName( |
| UmaStatsReportingDelegate::kUnusedAndClosedInIntervalHistogramNameBase, |
| interval); |
| std::string unused_histogram_name = GetIntervalHistogramName( |
| UmaStatsReportingDelegate::kUnusedTabsInIntervalHistogramNameBase, |
| interval); |
| |
| base::UmaHistogramCounts10000(used_and_closed_histogram_name, |
| used_and_closed_tabs); |
| base::UmaHistogramCounts10000(used_histogram_name, used_tabs); |
| base::UmaHistogramCounts10000(unused_and_closed_histogram_name, |
| unused_and_closed_tabs); |
| base::UmaHistogramCounts10000(unused_histogram_name, unused_tabs); |
| } |
| |
| // static |
| std::string |
| TabStatsTracker::UmaStatsReportingDelegate::GetIntervalHistogramName( |
| const char* base_name, |
| base::TimeDelta interval) { |
| DCHECK(IsValidInterval(interval)); |
| return base::StringPrintf("%s_%zu", base_name, |
| static_cast<size_t>(interval.InSeconds())); |
| } |
| |
| bool TabStatsTracker::UmaStatsReportingDelegate:: |
| IsChromeBackgroundedWithoutWindows() { |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| if (g_browser_process && g_browser_process->background_mode_manager() |
| ->IsBackgroundWithoutWindows()) { |
| return true; |
| } |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| return false; |
| } |
| |
| } // namespace metrics |