blob: acc34dc11c0c209bc74b7b24a2180c7301b6e31c [file] [log] [blame]
// Copyright 2013 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 "ios/chrome/browser/metrics/tab_usage_recorder.h"
#import <UIKit/UIKit.h>
#include "base/metrics/histogram_macros.h"
#include "ios/chrome/browser/chrome_url_constants.h"
#import "ios/chrome/browser/metrics/previous_session_info.h"
#import "ios/chrome/browser/prerender/prerender_service.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.h"
#import "ios/web/public/web_state/navigation_context.h"
#import "ios/web/public/web_state/web_state.h"
#include "ui/base/page_transition_types.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// The histogram recording the state of the tab the user switches to.
const char kSelectedTabHistogramName[] =
"Tab.StatusWhenSwitchedBackToForeground";
// The histogram to record the number of page loads before an evicted tab is
// selected.
const char kPageLoadsBeforeEvictedTabSelected[] =
"Tab.PageLoadsSinceLastSwitchToEvictedTab";
// Records the time it takes for an evicted tab to reload.
const char kEvictedTabReloadTime[] = "Tab.RestoreTime";
// Records success vs failure of an evicted tab's reload.
const char kEvictedTabReloadSuccessRate[] = "Tab.RestoreResult";
// Records whether or not the user switched tabs before an evicted tab finished
// reloading.
const char kDidUserWaitForEvictedTabReload[] = "Tab.RestoreUserPersistence";
// The name of the histogram that records time intervals between tab restores.
const char kTimeBetweenRestores[] = "Tabs.TimeBetweenRestores";
// The name of the histogram that records time intervals since the last restore.
const char kTimeAfterLastRestore[] = "Tabs.TimeAfterLastRestore";
// Name of histogram to record whether a memory warning had been recently
// received when a renderer termination occurred.
const char kRendererTerminationSawMemoryWarning[] =
"Tab.RendererTermination.RecentlyReceivedMemoryWarning";
// Name of histogram to record the number of alive renderers when a renderer
// termination is received.
const char kRendererTerminationAliveRenderers[] =
"Tab.RendererTermination.AliveRenderersCount";
// Name of histogram to record the number of renderers that were alive shortly
// before a renderer termination. This metric is being recorded in case the OS
// kills renderers in batches.
const char kRendererTerminationRecentlyAliveRenderers[] =
"Tab.RendererTermination.RecentlyAliveRenderersCount";
// Name of histogram for recording the state of the tab when the renderer is
// terminated.
const char kRendererTerminationStateHistogram[] =
"Tab.StateAtRendererTermination";
// The recently alive renderer count metric counts all renderers that were alive
// x seconds before a renderer termination. |kSecondsBeforeRendererTermination|
// specifies x.
const int kSecondsBeforeRendererTermination = 2;
TabUsageRecorder::TabUsageRecorder(WebStateList* web_state_list,
PrerenderService* prerender_service)
: restore_start_time_(base::TimeTicks::Now()),
web_state_list_(web_state_list),
prerender_service_(prerender_service) {
DCHECK(web_state_list_);
web_state_list_->AddObserver(this);
for (int index = 0; index < web_state_list_->count(); ++index) {
web::WebState* web_state = web_state_list_->GetWebStateAt(index);
web_state->AddObserver(this);
}
// Register for backgrounding and foregrounding notifications. It is safe for
// the block to capture a pointer to |this| as they are unregistered in the
// destructor and thus the block are not called after the end of its lifetime.
application_backgrounding_observer_ = [[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:nil
usingBlock:^(NSNotification*) {
this->AppDidEnterBackground();
}];
application_foregrounding_observer_ = [[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationWillEnterForegroundNotification
object:nil
queue:nil
usingBlock:^(NSNotification*) {
this->AppWillEnterForeground();
}];
}
TabUsageRecorder::~TabUsageRecorder() {
DCHECK(web_state_list_);
for (int index = 0; index < web_state_list_->count(); ++index) {
web::WebState* web_state = web_state_list_->GetWebStateAt(index);
web_state->RemoveObserver(this);
}
web_state_list_->RemoveObserver(this);
if (application_backgrounding_observer_) {
[[NSNotificationCenter defaultCenter]
removeObserver:application_backgrounding_observer_];
application_backgrounding_observer_ = nil;
}
if (application_foregrounding_observer_) {
[[NSNotificationCenter defaultCenter]
removeObserver:application_foregrounding_observer_];
application_foregrounding_observer_ = nil;
}
}
void TabUsageRecorder::InitialRestoredTabs(
web::WebState* active_web_state,
const std::vector<web::WebState*>& web_states) {
#if !defined(NDEBUG)
// Debugging check to ensure this is called at most once per run.
// Specifically, this function is called in either of two cases:
// 1. For a normal (not post-crash launch), during the tab model's creation.
// It assumes that the tab model will not be deleted and recreated during the
// application's lifecycle even if the app is backgrounded/foregrounded.
// 2. For a post-crash launch, when the session is restored. In that case,
// the tab model will not have been created with existing tabs, so this
// function will not have been called during its creation.
static bool kColdStartTabsRecorded = false;
static dispatch_once_t once = 0;
dispatch_once(&once, ^{
DCHECK(kColdStartTabsRecorded == false);
kColdStartTabsRecorded = true;
});
#endif
// Do not set eviction reason on active tab since it will be reloaded without
// being processed as a switch to the foreground tab.
for (web::WebState* web_state : web_states) {
if (web_state != active_web_state) {
evicted_web_states_[web_state] = EVICTED_DUE_TO_COLD_START;
}
}
}
void TabUsageRecorder::RecordTabSwitched(web::WebState* old_web_state,
web::WebState* new_web_state) {
// If a tab was created to be selected, and is selected shortly thereafter,
// it should not add its state to the "kSelectedTabHistogramName" metric.
// |web_state_created_selected_| is reset at the first tab switch seen after
// it was created, regardless of whether or not it was the tab selected.
const bool was_just_created = new_web_state == web_state_created_selected_;
web_state_created_selected_ = nullptr;
// Disregard reselecting the same tab, but only if the mode has not changed
// since the last time this tab was selected. I.e. going to incognito and
// back to normal mode is an event we want to track, but simply going into
// stack view and back out, without changing modes, isn't.
if (new_web_state == old_web_state && new_web_state != mode_switch_web_state_)
return;
mode_switch_web_state_ = nullptr;
// Disregard opening a new tab with no previous tab. Or closing the last tab.
if (!old_web_state || !new_web_state)
return;
// Before knowledge of the previous tab, |old_web_state|, is lost, see if it
// is a previously-evicted tab still reloading. If it is, record that the
// user did not wait for the evicted tab to finish reloading.
if (old_web_state == evicted_web_state_ && old_web_state != new_web_state &&
evicted_web_state_reload_start_time_ != base::TimeTicks()) {
UMA_HISTOGRAM_ENUMERATION(kDidUserWaitForEvictedTabReload,
USER_DID_NOT_WAIT, USER_BEHAVIOR_COUNT);
}
ResetEvictedTab();
if (ShouldIgnoreWebState(new_web_state) || was_just_created)
return;
// Should never happen. Keeping the check to ensure that the prerender logic
// is never overlooked, should behavior at the tab_model level change.
DCHECK(!prerender_service_ ||
!prerender_service_->IsWebStatePrerendered(new_web_state));
TabStateWhenSelected web_state_state = ExtractWebStateState(new_web_state);
if (web_state_state != IN_MEMORY) {
// Keep track of the current 'evicted' tab.
evicted_web_state_ = new_web_state;
evicted_web_state_state_ = web_state_state;
UMA_HISTOGRAM_COUNTS_1M(kPageLoadsBeforeEvictedTabSelected, page_loads_);
ResetPageLoads();
}
UMA_HISTOGRAM_ENUMERATION(kSelectedTabHistogramName, web_state_state,
TAB_STATE_COUNT);
}
void TabUsageRecorder::RecordPrimaryTabModelChange(
bool primary_tab_model,
web::WebState* active_web_state) {
if (primary_tab_model) {
// User just came back to this tab model, so record a tab selection even
// though the current tab was reselected.
if (mode_switch_web_state_ == active_web_state)
RecordTabSwitched(active_web_state, active_web_state);
} else {
// Keep track of the selected tab when this tab model is moved to
// background. This way when the tab model is moved to the foreground, and
// the current tab reselected, it is handled as a tab selection rather than
// a no-op.
mode_switch_web_state_ = active_web_state;
}
}
void TabUsageRecorder::RecordPageLoadStart(web::WebState* web_state) {
if (!ShouldIgnoreWebState(web_state)) {
page_loads_++;
if (web_state->IsEvicted()) {
// On the iPad, there is no notification that a tab is being re-selected
// after changing modes. This catches the case where the pre-incognito
// selected tab is selected again when leaving incognito mode.
if (mode_switch_web_state_ == web_state)
RecordTabSwitched(web_state, web_state);
if (evicted_web_state_ == web_state)
RecordRestoreStartTime();
}
} else {
// If there is a currently-evicted tab reloading, make sure it is recorded
// that the user did not wait for it to load.
if (evicted_web_state_ &&
evicted_web_state_reload_start_time_ != base::TimeTicks()) {
UMA_HISTOGRAM_ENUMERATION(kDidUserWaitForEvictedTabReload,
USER_DID_NOT_WAIT, USER_BEHAVIOR_COUNT);
}
ResetEvictedTab();
}
}
void TabUsageRecorder::RecordPageLoadDone(web::WebState* web_state,
bool success) {
if (!web_state)
return;
if (web_state == evicted_web_state_) {
if (success) {
LOCAL_HISTOGRAM_TIMES(
kEvictedTabReloadTime,
base::TimeTicks::Now() - evicted_web_state_reload_start_time_);
}
UMA_HISTOGRAM_ENUMERATION(kEvictedTabReloadSuccessRate,
success ? LOAD_SUCCESS : LOAD_FAILURE,
LOAD_DONE_STATE_COUNT);
UMA_HISTOGRAM_ENUMERATION(kDidUserWaitForEvictedTabReload, USER_WAITED,
USER_BEHAVIOR_COUNT);
ResetEvictedTab();
}
}
void TabUsageRecorder::RecordReload(web::WebState* web_state) {
if (!ShouldIgnoreWebState(web_state)) {
page_loads_++;
}
}
void TabUsageRecorder::RendererTerminated(web::WebState* terminated_web_state,
bool web_state_visible,
bool application_active) {
// Log the tab state for the termination.
const RendererTerminationTabState web_state_state =
application_active ? (web_state_visible ? FOREGROUND_TAB_FOREGROUND_APP
: BACKGROUND_TAB_FOREGROUND_APP)
: (web_state_visible ? FOREGROUND_TAB_BACKGROUND_APP
: BACKGROUND_TAB_BACKGROUND_APP);
UMA_HISTOGRAM_ENUMERATION(kRendererTerminationStateHistogram,
static_cast<int>(web_state_state),
static_cast<int>(TERMINATION_TAB_STATE_COUNT));
if (!web_state_visible) {
DCHECK(!WebStateAlreadyEvicted(terminated_web_state));
evicted_web_states_[terminated_web_state] =
EVICTED_DUE_TO_RENDERER_TERMINATION;
}
base::TimeTicks now = base::TimeTicks::Now();
termination_timestamps_.push_back(now);
// Log if a memory warning was seen recently.
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
BOOL saw_memory_warning =
[defaults boolForKey:previous_session_info_constants::
kDidSeeMemoryWarningShortlyBeforeTerminating];
UMA_HISTOGRAM_BOOLEAN(kRendererTerminationSawMemoryWarning,
saw_memory_warning);
// Log number of live tabs after the renderer termination. This count does not
// include |terminated_web_state|.
int live_web_states_count = GetLiveWebStatesCount();
UMA_HISTOGRAM_COUNTS_100(kRendererTerminationAliveRenderers,
live_web_states_count);
// Clear |termination_timestamps_| of timestamps older than
// |kSecondsBeforeRendererTermination| ago.
base::TimeDelta seconds_before =
base::TimeDelta::FromSeconds(kSecondsBeforeRendererTermination);
base::TimeTicks timestamp_boundary = now - seconds_before;
while (termination_timestamps_.front() < timestamp_boundary) {
termination_timestamps_.pop_front();
}
// Log number of recently alive tabs, where recently alive is defined to mean
// alive within the past |kSecondsBeforeRendererTermination|.
NSUInteger recently_live_web_states_count =
live_web_states_count + termination_timestamps_.size();
UMA_HISTOGRAM_COUNTS_100(kRendererTerminationRecentlyAliveRenderers,
recently_live_web_states_count);
}
void TabUsageRecorder::AppDidEnterBackground() {
base::TimeTicks time_now = base::TimeTicks::Now();
LOCAL_HISTOGRAM_TIMES(kTimeAfterLastRestore, time_now - restore_start_time_);
if (evicted_web_state_ &&
evicted_web_state_reload_start_time_ != base::TimeTicks()) {
UMA_HISTOGRAM_ENUMERATION(kDidUserWaitForEvictedTabReload, USER_LEFT_CHROME,
USER_BEHAVIOR_COUNT);
ResetEvictedTab();
}
}
void TabUsageRecorder::AppWillEnterForeground() {
restore_start_time_ = base::TimeTicks::Now();
}
void TabUsageRecorder::ResetPageLoads() {
page_loads_ = 0;
}
int TabUsageRecorder::EvictedTabsMapSize() {
return evicted_web_states_.size();
}
void TabUsageRecorder::ResetAll() {
ResetEvictedTab();
ResetPageLoads();
evicted_web_states_.clear();
}
void TabUsageRecorder::ResetEvictedTab() {
evicted_web_state_ = nullptr;
evicted_web_state_state_ = IN_MEMORY;
evicted_web_state_reload_start_time_ = base::TimeTicks();
}
bool TabUsageRecorder::ShouldIgnoreWebState(web::WebState* web_state) {
// Do not count chrome:// urls to avoid data noise. For example, if they were
// counted, every new tab created would add noise to the page load count.
web::NavigationItem* pending_item =
web_state->GetNavigationManager()->GetPendingItem();
if (pending_item)
return pending_item->GetVirtualURL().SchemeIs(kChromeUIScheme);
web::NavigationItem* last_committed_item =
web_state->GetNavigationManager()->GetLastCommittedItem();
if (last_committed_item)
return last_committed_item->GetVirtualURL().SchemeIs(kChromeUIScheme);
return false;
}
bool TabUsageRecorder::WebStateAlreadyEvicted(web::WebState* web_state) {
auto iter = evicted_web_states_.find(web_state);
return iter != evicted_web_states_.end();
}
TabUsageRecorder::TabStateWhenSelected TabUsageRecorder::ExtractWebStateState(
web::WebState* web_state) {
if (!web_state->IsEvicted())
return IN_MEMORY;
auto iter = evicted_web_states_.find(web_state);
if (iter != evicted_web_states_.end()) {
TabStateWhenSelected web_state_state = iter->second;
evicted_web_states_.erase(iter);
return web_state_state;
}
return EVICTED;
}
void TabUsageRecorder::RecordRestoreStartTime() {
base::TimeTicks time_now = base::TimeTicks::Now();
// Record the time delta since the last eviction reload was seen.
LOCAL_HISTOGRAM_TIMES(kTimeBetweenRestores, time_now - restore_start_time_);
restore_start_time_ = time_now;
evicted_web_state_reload_start_time_ = time_now;
}
int TabUsageRecorder::GetLiveWebStatesCount() const {
int count = 0;
for (int index = 0; index < web_state_list_->count(); ++index) {
if (!web_state_list_->GetWebStateAt(index)->IsEvicted())
++count;
}
return count;
}
void TabUsageRecorder::OnWebStateDestroyed(web::WebState* web_state) {
if (web_state == web_state_created_selected_)
web_state_created_selected_ = nullptr;
if (web_state == evicted_web_state_)
evicted_web_state_ = nullptr;
if (web_state == mode_switch_web_state_)
mode_switch_web_state_ = nullptr;
auto evicted_web_states_iter = evicted_web_states_.find(web_state);
if (evicted_web_states_iter != evicted_web_states_.end())
evicted_web_states_.erase(evicted_web_states_iter);
web_state->RemoveObserver(this);
}
void TabUsageRecorder::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (PageTransitionCoreTypeIs(navigation_context->GetPageTransition(),
ui::PAGE_TRANSITION_RELOAD)) {
RecordReload(web_state);
}
}
void TabUsageRecorder::PageLoaded(
web::WebState* web_state,
web::PageLoadCompletionStatus load_completion_status) {
RecordPageLoadDone(web_state, load_completion_status ==
web::PageLoadCompletionStatus::SUCCESS);
}
void TabUsageRecorder::RenderProcessGone(web::WebState* web_state) {
bool is_active;
switch ([UIApplication sharedApplication].applicationState) {
case UIApplicationStateActive:
is_active = true;
break;
case UIApplicationStateInactive:
case UIApplicationStateBackground:
is_active = false;
break;
}
RendererTerminated(web_state, web_state->IsVisible(), is_active);
}
void TabUsageRecorder::WebStateDestroyed(web::WebState* web_state) {
// TabUsageRecorder only watches WebState inserted in a WebStateList. The
// WebStateList owns the WebStates it manages. TabUsageRecorder removes
// itself from WebStates' WebStateObservers when notified by WebStateList
// that a WebState is removed, so it should never notice WebStateDestroyed
// event. Thus the implementation enforces this with NOTREACHED().
NOTREACHED();
}
void TabUsageRecorder::WebStateInsertedAt(WebStateList* web_state_list,
web::WebState* web_state,
int index,
bool activating) {
if (activating)
web_state_created_selected_ = web_state;
web_state->AddObserver(this);
}
void TabUsageRecorder::WebStateReplacedAt(WebStateList* web_state_list,
web::WebState* old_web_state,
web::WebState* new_web_state,
int index) {
OnWebStateDestroyed(old_web_state);
if (new_web_state)
new_web_state->AddObserver(this);
}
void TabUsageRecorder::WebStateDetachedAt(WebStateList* web_state_list,
web::WebState* web_state,
int index) {
OnWebStateDestroyed(web_state);
}
void TabUsageRecorder::WebStateActivatedAt(WebStateList* web_state_list,
web::WebState* old_web_state,
web::WebState* new_web_state,
int active_index,
int reason) {
if (reason & WebStateListObserver::CHANGE_REASON_USER_ACTION)
RecordTabSwitched(old_web_state, new_web_state);
}