blob: 019c4d41615eda00c1295baaeeb773fd5d967ee5 [file] [log] [blame]
// 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/resource_coordinator/tab_activity_watcher.h"
#include <limits>
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/resource_coordinator/lifecycle_unit.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h"
#include "chrome/browser/resource_coordinator/tab_manager_features.h"
#include "chrome/browser/resource_coordinator/tab_metrics_logger.h"
#include "chrome/browser/resource_coordinator/tab_ranker/mru_features.h"
#include "chrome/browser/resource_coordinator/tab_ranker/tab_features.h"
#include "chrome/browser/resource_coordinator/time.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.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 "components/ukm/content/source_url_recorder.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "third_party/blink/public/common/input/web_input_event.h"
namespace resource_coordinator {
namespace {
using tab_ranker::TabFeatures;
// Used for decay Frecency scores.
constexpr float kFrecencyScoreDecay = 0.8f;
// Records how many tab reactivations till now.
static int32_t reactivation_index = 0;
// Used for generating label_ids and query_ids.
int64_t internal_id_for_logging = 0;
// Returns an int64_t number as label_id or query_id.
int64_t NewInt64ForLabelIdOrQueryId() {
// The id is shifted 16 bits so that the lower bits are reserved for counting
// multiple queries.
// We choose 16 so that the lower bits for counting multiple queries and
// higher bits for labeling queries are both unlikely to overflow. (lower bits
// only overflows when we have more than 65536 queries without labeling
// events; higher bits only overflow when we have more than 100 billion
// discards.
constexpr int kIdShiftBits = 16;
return (++internal_id_for_logging) << kIdShiftBits;
}
} // namespace
// Per-WebContents helper class that observes its WebContents, notifying
// TabActivityWatcher when interesting events occur. Also provides
// per-WebContents data that TabActivityWatcher uses to log the tab.
class TabActivityWatcher::WebContentsData
: public content::WebContentsObserver,
public content::WebContentsUserData<WebContentsData>,
public content::RenderWidgetHost::InputEventObserver {
public:
~WebContentsData() override = default;
// Calculates the tab reactivation score for a background tab. Returns nullopt
// if the score could not be calculated, e.g. because the tab is in the
// foreground.
base::Optional<float> CalculateReactivationScore() {
if (web_contents()->IsBeingDestroyed() || backgrounded_time_.is_null())
return base::nullopt;
// No log for CalculateReactivationScore.
base::Optional<TabFeatures> tab = GetTabFeatures();
if (!tab.has_value())
return base::nullopt;
float score = 0.0f;
const tab_ranker::TabRankerResult result =
TabActivityWatcher::GetInstance()->predictor_->ScoreTab(tab.value(),
&score);
if (result == tab_ranker::TabRankerResult::kSuccess)
return score;
return base::nullopt;
}
// Call when the associated WebContents has been replaced.
void WasReplaced() { was_replaced_ = true; }
// Call when the associated WebContents has replaced the WebContents of
// another tab. Copies info from the other WebContentsData so future events
// can be logged consistently.
void DidReplace(const WebContentsData& replaced_tab) {
// Copy creation and foregrounded times to retain the replaced tab's MRU
// position.
creation_time_ = replaced_tab.creation_time_;
foregrounded_time_ = replaced_tab.foregrounded_time_;
// Copy background status so ForegroundOrClosed can potentially be logged.
backgrounded_time_ = replaced_tab.backgrounded_time_;
// Copy the replaced tab's stats.
page_metrics_ = replaced_tab.page_metrics_;
// Recover the ukm_source_id from the |replaced_tab|.
ukm_source_id_ = replaced_tab.ukm_source_id_;
// Copy the replaced label_id_.
label_id_ = replaced_tab.label_id_;
// Copy the frecency score.
frecency_score_ = replaced_tab.frecency_score_;
}
// Call when the WebContents is detached from its tab. If the tab is later
// re-inserted elsewhere, we use the state it had before being detached.
void TabDetached() { is_detached_ = true; }
// Call when the tab is inserted into a tab strip to update state.
void TabInserted(bool foreground) {
if (is_detached_) {
is_detached_ = false;
// Dragged tabs are normally inserted into their new tab strip in the
// "background", then "activated", even though the user perceives the tab
// staying active the whole time. So don't update |background_time_| here.
//
// TODO(michaelpg): If a background tab is dragged (as part of a group)
// and inserted, it may be treated as being foregrounded (depending on tab
// order). This is a small edge case, but can be fixed by the plan to
// merge the ForegroundedOrClosed and TabMetrics events.
return;
}
if (foreground) {
foregrounded_time_ = NowTicks();
UpdateFrecencyScoreOnReactivation();
} else {
// This is a new tab that was opened in the background.
backgrounded_time_ = NowTicks();
}
}
// Logs TabMetrics for the tab if it is considered to be backgrounded.
void LogTabIfBackgrounded() {
if (backgrounded_time_.is_null() || DisableBackgroundLogWithTabRanker())
return;
base::Optional<TabFeatures> tab = GetTabFeatures();
if (tab.has_value()) {
// Background time logging always logged with label_id == 0, since we
// only use label_id for query time logging for now.
TabActivityWatcher::GetInstance()->tab_metrics_logger_->LogTabMetrics(
ukm_source_id_, tab.value(), web_contents(), 0);
}
}
// Logs current TabFeatures; skips if current tab is null.
void LogCurrentTabFeatures(const base::Optional<TabFeatures>& tab) {
if (!tab.has_value())
return;
// Update label_id_: a new label_id is generated for this query if the
// label_id_ is 0; otherwise the old label_id_ is incremented. This allows
// us to better pairing TabMetrics with ForegroundedOrClosed events offline.
// The same label_id_ will be logged with ForegroundedOrClosed event later
// on so that TabFeatures can be paired with ForegroundedOrClosed.
label_id_ = label_id_ ? label_id_ + 1 : NewInt64ForLabelIdOrQueryId();
TabActivityWatcher::GetInstance()->tab_metrics_logger_->LogTabMetrics(
ukm_source_id_, tab.value(), web_contents(), label_id_);
}
// Sets foregrounded_time_ to NowTicks() so this becomes the
// most-recently-used tab.
void TabWindowActivated() { foregrounded_time_ = NowTicks(); }
private:
friend class content::WebContentsUserData<WebContentsData>;
friend class TabActivityWatcher;
// A FrecencyScore is used as a measurement of both frequency and recency.
// (1) The score is decayed by kFrecencyScoreDecay every time any tab is
// reactivated.
// (2) The score is incremented by 1.0 - kFrecencyScoreDecay when this tab is
// reactivated.
struct FrecencyScore {
int32_t update_index = 0;
float score = 0.0f;
};
explicit WebContentsData(content::WebContents* web_contents)
: WebContentsObserver(web_contents) {
DCHECK(!web_contents->GetBrowserContext()->IsOffTheRecord());
web_contents->GetMainFrame()
->GetRenderViewHost()
->GetWidget()
->AddInputEventObserver(this);
creation_time_ = NowTicks();
// A navigation may already have completed if this is a replacement tab.
ukm_source_id_ = ukm::GetSourceIdForWebContentsDocument(web_contents);
// When a tab is discarded, a new null_web_contents will be created (with
// WasDiscarded set as true) applied as a replacement of the discarded tab.
// We want to record this discarded state for later logging.
discarded_since_backgrounded_ = web_contents->WasDiscarded();
}
void WasHidden() {
// The tab may not be in the tabstrip if it's being moved or replaced.
Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
if (!browser)
return;
DCHECK(!browser->tab_strip_model()->closing_all());
if (browser->tab_strip_model()->GetActiveWebContents() == web_contents() &&
!browser->window()->IsMinimized()) {
// The active tab is considered to be in the foreground unless its window
// is minimized. It might still get hidden, e.g. when the browser is about
// to close, but that shouldn't count as a backgrounded event.
//
// TODO(michaelpg): On Mac, hiding the application (e.g. via Cmd+H) should
// log tabs as backgrounded. Check NSApplication's isHidden property.
return;
}
backgrounded_time_ = NowTicks();
discarded_since_backgrounded_ = false;
LogTabIfBackgrounded();
}
void WasShown() {
UpdateFrecencyScoreOnReactivation();
if (backgrounded_time_.is_null())
return;
Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
if (browser && browser->tab_strip_model()->closing_all())
return;
// Log the event before updating times.
LogForegroundedOrClosedMetrics(true /* is_foregrounded */);
backgrounded_time_ = base::TimeTicks();
foregrounded_time_ = NowTicks();
page_metrics_.num_reactivations++;
}
// content::WebContentsObserver:
void RenderViewHostChanged(content::RenderViewHost* old_host,
content::RenderViewHost* new_host) override {
if (old_host != nullptr)
old_host->GetWidget()->RemoveInputEventObserver(this);
new_host->GetWidget()->AddInputEventObserver(this);
}
void DidFinishNavigation(
content::NavigationHandle* navigation_handle) override {
if (!navigation_handle->HasCommitted() ||
!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
// Use the same SourceId that SourceUrlRecorderWebContentsObserver populates
// and updates.
ukm::SourceId new_source_id = ukm::ConvertToSourceId(
navigation_handle->GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID);
DCHECK_NE(new_source_id, ukm_source_id_)
<< "Expected a unique Source ID for the navigation";
ukm_source_id_ = new_source_id;
// Reset the per-page data.
page_metrics_ = {};
// Update navigation info.
page_metrics_.page_transition = navigation_handle->GetPageTransition();
}
// Logs metrics for the tab when it stops loading instead of immediately
// after a navigation commits, so we can have some idea of its status and
// contents.
void DidStopLoading() override {
// Ignore load events in foreground tabs. The tab state of a foreground tab
// will be logged if/when it is backgrounded.
LogTabIfBackgrounded();
}
void OnVisibilityChanged(content::Visibility visibility) override {
// Record background tab UKMs and do associated bookkepping.
if (!web_contents()->IsBeingDestroyed()) {
// TODO(michaelpg): Consider treating occluded tabs as hidden.
if (visibility == content::Visibility::HIDDEN) {
WasHidden();
} else {
WasShown();
}
}
}
void WebContentsDestroyed() override {
if (was_replaced_)
return;
// Log necessary metrics.
TabActivityWatcher::GetInstance()->OnTabClosed(this);
}
// content::RenderWidgetHost::InputEventObserver:
void OnInputEvent(const blink::WebInputEvent& event) override {
if (blink::WebInputEvent::IsMouseEventType(event.GetType()))
page_metrics_.mouse_event_count++;
else if (blink::WebInputEvent::IsKeyboardEventType(event.GetType()))
page_metrics_.key_event_count++;
else if (blink::WebInputEvent::IsTouchEventType(event.GetType()))
page_metrics_.touch_event_count++;
}
// Iterates through tabstrips to determine the index of |contents| in
// most-recently-used order out of all non-incognito tabs.
// Linear in the number of tabs (most users have <10 tabs open).
tab_ranker::MRUFeatures GetMRUFeatures() {
// If not in closing_all mode, calculate |mru_features_|.
mru_features_.index = 0;
mru_features_.total = 0;
for (Browser* browser : *BrowserList::GetInstance()) {
// Ignore incognito browsers.
if (browser->profile()->IsOffTheRecord())
continue;
int count = browser->tab_strip_model()->count();
mru_features_.total += count;
// Increment the MRU index for each WebContents that was foregrounded more
// recently than this one.
for (int i = 0; i < count; i++) {
auto* other = WebContentsData::FromWebContents(
browser->tab_strip_model()->GetWebContentsAt(i));
if (!other || this == other)
continue;
if (!MoreRecentlyUsed(this, other))
mru_features_.index++;
}
}
return mru_features_;
}
// Returns whether |webcontents_a| is more recently used than |webcontents_b|.
// A webcontents is more recently used iff it has larger (later)
// |foregrounded_time_|; or |creation_time_| if they were never foregrounded.
static bool MoreRecentlyUsed(
TabActivityWatcher::WebContentsData* webcontents_a,
TabActivityWatcher::WebContentsData* const webcontents_b) {
return webcontents_a->foregrounded_time_ >
webcontents_b->foregrounded_time_ ||
(webcontents_a->foregrounded_time_ ==
webcontents_b->foregrounded_time_ &&
webcontents_a->creation_time_ > webcontents_b->creation_time_);
}
// Returns the tabfeatures of current tab by combining TabMetrics,
// WindowFeatures and MRUFeatures.
// TODO(charleszhao): refactor TabMetricsLogger::GetTabFeatures to return a
// full TabFeatures instead of a partial TabFeatures.
base::Optional<TabFeatures> GetTabFeatures() {
if (web_contents()->IsBeingDestroyed() || backgrounded_time_.is_null())
return base::nullopt;
// For tab features.
base::Optional<TabFeatures> tab =
TabMetricsLogger::GetTabFeatures(page_metrics_, web_contents());
if (!tab.has_value())
return tab;
tab->time_from_backgrounded =
backgrounded_time_.is_null()
? 0
: (NowTicks() - backgrounded_time_).InMilliseconds();
// For mru features.
const tab_ranker::MRUFeatures& mru = GetMRUFeatures();
tab->mru_index = mru.index;
tab->total_tab_count = mru.total;
// For frecency_score;
tab->frecency_score = GetFrecencyScore();
return tab;
}
// Collect current ForegroundedOrClosedMetrics and send to ukm.
void LogForegroundedOrClosedMetrics(bool is_foregrounded) {
// If background time logging is disabled, then we only log the case where
// the label_id_ != 0 (a feature is logged and a label has not been logged).
if (DisableBackgroundLogWithTabRanker() && label_id_ == 0)
return;
TabMetricsLogger::ForegroundedOrClosedMetrics metrics;
metrics.is_foregrounded = is_foregrounded;
metrics.is_discarded = discarded_since_backgrounded_;
metrics.time_from_backgrounded =
(NowTicks() - backgrounded_time_).InMilliseconds();
metrics.label_id = label_id_;
TabActivityWatcher::GetInstance()
->tab_metrics_logger_->LogForegroundedOrClosedMetrics(ukm_source_id_,
metrics);
// label_id_ is reset whenever a label is logged.
// A new label_id_ is generated when a query happens inside
// CalculateReactivationScore, after that this ForegroundedOrClosed logging
// can happen many times (tabs may get backgrounded and reactivated several
// times). In such cases, we only count the first time as the true label,
// the rest are considered to be query time logging irrelevant, for which we
// log with label_id == 0.
label_id_ = 0;
}
// Returns frecency score of this tab.
// NOTE: we don't apply decay for all reactivations, instead we accumulate
// them as reactivations_since_last_update and applied all together when the
// score is queried.
float GetFrecencyScore() {
const int reactivations_since_last_update =
reactivation_index - frecency_score_.update_index;
if (reactivations_since_last_update > 0) {
frecency_score_.score *=
std::pow(kFrecencyScoreDecay, reactivations_since_last_update);
frecency_score_.update_index = reactivation_index;
}
return frecency_score_.score;
}
// Updates frecency score of current tab when it is reactivated.
void UpdateFrecencyScoreOnReactivation() {
++reactivation_index;
// Updates the current score.
frecency_score_.score = GetFrecencyScore() + 1.0f - kFrecencyScoreDecay;
}
// Updated when a navigation is finished.
ukm::SourceId ukm_source_id_ = 0;
// When the tab was created.
base::TimeTicks creation_time_;
// The most recent time the tab became backgrounded. This happens when a
// different tab in the tabstrip is activated or the tab's window is hidden.
base::TimeTicks backgrounded_time_;
// The most recent time the tab became foregrounded. This happens when the
// tab becomes the active tab in the tabstrip or when the active tab's window
// is activated.
base::TimeTicks foregrounded_time_;
// Stores current page stats for the tab.
TabMetricsLogger::PageMetrics page_metrics_;
// Set to true when the WebContents has been detached from its tab.
bool is_detached_ = false;
// If true, future events such as the tab being destroyed won't be logged.
bool was_replaced_ = false;
// MRUFeatures of this WebContents, updated only before ForegroundedOrClosed
// event is logged.
tab_ranker::MRUFeatures mru_features_;
// Whether this tab is currently in discarded state.
bool discarded_since_backgrounded_ = false;
// An int64 random label to pair TabFeatures with ForegroundedOrClosed event.
int64_t label_id_ = 0;
// Fecency score of this tab.
FrecencyScore frecency_score_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
DISALLOW_COPY_AND_ASSIGN(WebContentsData);
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(TabActivityWatcher::WebContentsData)
TabActivityWatcher::TabActivityWatcher()
: tab_metrics_logger_(std::make_unique<TabMetricsLogger>()),
browser_tab_strip_tracker_(this, this),
predictor_(std::make_unique<tab_ranker::TabScorePredictor>()) {
BrowserList::AddObserver(this);
browser_tab_strip_tracker_.Init();
}
TabActivityWatcher::~TabActivityWatcher() {
BrowserList::RemoveObserver(this);
}
base::Optional<float> TabActivityWatcher::CalculateReactivationScore(
content::WebContents* web_contents) {
WebContentsData* web_contents_data =
WebContentsData::FromWebContents(web_contents);
if (!web_contents_data)
return base::nullopt;
return web_contents_data->CalculateReactivationScore();
}
void TabActivityWatcher::LogAndMaybeSortLifecycleUnitWithTabRanker(
std::vector<LifecycleUnit*>* tabs) {
// Set query_id so that all TabFeatures logged in this query can be joined.
tab_metrics_logger_->set_query_id(NewInt64ForLabelIdOrQueryId());
const bool should_sort_tabs =
base::FeatureList::IsEnabled(features::kTabRanker);
std::map<int32_t, base::Optional<TabFeatures>> tab_features;
for (auto* lifecycle_unit : *tabs) {
auto* lifecycle_unit_external =
lifecycle_unit->AsTabLifecycleUnitExternal();
// the lifecycle_unit_external is nullptr in the unit test
// TabManagerDelegateTest::KillMultipleProcesses.
if (!lifecycle_unit_external) {
tab_features[lifecycle_unit->GetID()] = base::nullopt;
continue;
}
WebContentsData* web_contents_data = WebContentsData::FromWebContents(
lifecycle_unit_external->GetWebContents());
// The web_contents_data can be nullptr in some cases.
// TODO(crbug.com/1019482): move the creation of WebContentsData to
// TabHelpers::AttachTabHelpers.
if (!web_contents_data) {
tab_features[lifecycle_unit->GetID()] = base::nullopt;
continue;
}
const base::Optional<TabFeatures> tab = web_contents_data->GetTabFeatures();
web_contents_data->LogCurrentTabFeatures(tab);
// No reason to store TabFeatures if TabRanker is disabled.
if (should_sort_tabs) {
tab_features[lifecycle_unit->GetID()] = tab;
}
}
// Directly return if TabRanker is disabled.
if (!should_sort_tabs)
return;
const std::map<int32_t, float> reactivation_scores =
predictor_->ScoreTabs(tab_features);
// Sort with larger reactivation_score first (desending importance).
std::sort(tabs->begin(), tabs->end(),
[&reactivation_scores](const LifecycleUnit* const a,
const LifecycleUnit* const b) {
return reactivation_scores.at(a->GetID()) >
reactivation_scores.at(b->GetID());
});
}
void TabActivityWatcher::OnBrowserSetLastActive(Browser* browser) {
if (browser->tab_strip_model()->closing_all())
return;
content::WebContents* active_contents =
browser->tab_strip_model()->GetActiveWebContents();
if (!active_contents)
return;
// Don't assume the WebContentsData already exists in case activation happens
// before the tabstrip is fully updated.
WebContentsData* web_contents_data =
WebContentsData::FromWebContents(active_contents);
if (web_contents_data)
web_contents_data->TabWindowActivated();
}
void TabActivityWatcher::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
switch (change.type()) {
case TabStripModelChange::kInserted: {
for (const auto& contents : change.GetInsert()->contents) {
// Ensure the WebContentsData is created to observe this WebContents
// since it may represent a newly created tab.
WebContentsData::CreateForWebContents(contents.contents);
WebContentsData::FromWebContents(contents.contents)
->TabInserted(selection.new_contents == contents.contents);
}
break;
}
case TabStripModelChange::kRemoved: {
for (const auto& contents : change.GetRemove()->contents)
WebContentsData::FromWebContents(contents.contents)->TabDetached();
break;
}
case TabStripModelChange::kReplaced: {
auto* replace = change.GetReplace();
WebContentsData* old_web_contents_data =
WebContentsData::FromWebContents(replace->old_contents);
old_web_contents_data->WasReplaced();
// Ensure the WebContentsData is created to observe this WebContents
// since it likely hasn't been inserted into a tabstrip before.
WebContentsData::CreateForWebContents(replace->new_contents);
WebContentsData::FromWebContents(replace->new_contents)
->DidReplace(*old_web_contents_data);
break;
}
case TabStripModelChange::kMoved:
case TabStripModelChange::kSelectionOnly:
break;
}
}
void TabActivityWatcher::TabPinnedStateChanged(TabStripModel* tab_strip_model,
content::WebContents* contents,
int index) {
WebContentsData::FromWebContents(contents)->LogTabIfBackgrounded();
}
bool TabActivityWatcher::ShouldTrackBrowser(Browser* browser) {
// Don't track incognito browsers. This is also enforced by UKM.
// TODO(michaelpg): Keep counters for incognito browsers so we can score them
// using the TabScorePredictor. We should be able to do this without logging
// these values.
return !browser->profile()->IsOffTheRecord();
}
void TabActivityWatcher::ResetForTesting() {
tab_metrics_logger_ = std::make_unique<TabMetricsLogger>();
predictor_ = std::make_unique<tab_ranker::TabScorePredictor>();
internal_id_for_logging = 0;
}
// static
TabActivityWatcher* TabActivityWatcher::GetInstance() {
static base::NoDestructor<TabActivityWatcher> instance;
return instance.get();
}
void TabActivityWatcher::OnTabClosed(WebContentsData* web_contents_data) {
// Log ForegroundedOrClosed event.
if (!web_contents_data->backgrounded_time_.is_null()) {
web_contents_data->LogForegroundedOrClosedMetrics(
false /*is_foregrounded */);
}
}
} // namespace resource_coordinator