chromium/ios/chrome/browser/metrics/model/tab_usage_recorder_browser_agent.mm

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/metrics/model/tab_usage_recorder_browser_agent.h"

#import <UIKit/UIKit.h>

#import "base/metrics/histogram_macros.h"
#import "components/previous_session_info/previous_session_info.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/chrome/browser/prerender/model/prerender_service.h"
#import "ios/chrome/browser/prerender/model/prerender_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/components/webui/web_ui_url_constants.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
#import "services/metrics/public/cpp/ukm_builders.h"
#import "ui/base/page_transition_types.h"

BROWSER_USER_DATA_KEY_IMPL(TabUsageRecorderBrowserAgent)

TabUsageRecorderBrowserAgent::TabUsageRecorderBrowserAgent(Browser* browser)
    : restore_start_time_(base::TimeTicks::Now()),
      web_state_list_(browser->GetWebStateList()),
      prerender_service_(PrerenderServiceFactory::GetForBrowserState(
          browser->GetBrowserState())) {
  browser->AddObserver(this);

  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);
  }

  ChromeBrowserState* browser_state = browser->GetBrowserState();
  session_restoration_service_observation_.Observe(
      SessionRestorationServiceFactory::GetForBrowserState(browser_state));

  // 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();
              }];
}

TabUsageRecorderBrowserAgent::~TabUsageRecorderBrowserAgent() {
  DCHECK(!application_foregrounding_observer_);
  DCHECK(!application_backgrounding_observer_);
}

void TabUsageRecorderBrowserAgent::BrowserDestroyed(Browser* browser) {
  DCHECK_EQ(browser->GetWebStateList(), 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);
  browser->RemoveObserver(this);
  session_restoration_service_observation_.Reset();
  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;
  }
  web_state_list_ = nullptr;
}

void TabUsageRecorderBrowserAgent::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] =
          tab_usage_recorder::EVICTED_DUE_TO_COLD_START;
    }
  }
}

void TabUsageRecorderBrowserAgent::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;

  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));

  tab_usage_recorder::TabStateWhenSelected web_state_state =
      ExtractWebStateState(new_web_state);
  if (web_state_state != tab_usage_recorder::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(
        tab_usage_recorder::kPageLoadsBeforeEvictedTabSelected, page_loads_);
    ResetPageLoads();
  }

  UMA_HISTOGRAM_ENUMERATION(tab_usage_recorder::kSelectedTabHistogramName,
                            web_state_state,
                            tab_usage_recorder::TAB_STATE_COUNT);
}

void TabUsageRecorderBrowserAgent::RecordPrimaryBrowserChange(
    bool primary_browser) {
  web::WebState* active_web_state =
      web_state_list_ ? web_state_list_->GetActiveWebState() : nullptr;
  if (primary_browser) {
    // 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 TabUsageRecorderBrowserAgent::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 {
    ResetEvictedTab();
  }
}

void TabUsageRecorderBrowserAgent::RecordPageLoadDone(
    web::WebState* web_state) {
  if (!web_state)
    return;
  if (web_state == evicted_web_state_) {
    ResetEvictedTab();
  }
}

void TabUsageRecorderBrowserAgent::RecordReload(web::WebState* web_state) {
  if (!ShouldIgnoreWebState(web_state)) {
    page_loads_++;
  }
}

void TabUsageRecorderBrowserAgent::RendererTerminated(
    web::WebState* terminated_web_state,
    bool web_state_visible,
    bool application_active) {
  // Log the tab state for the termination.
  const tab_usage_recorder::RendererTerminationTabState web_state_state =
      application_active
          ? (web_state_visible
                 ? tab_usage_recorder::FOREGROUND_TAB_FOREGROUND_APP
                 : tab_usage_recorder::BACKGROUND_TAB_FOREGROUND_APP)
          : (web_state_visible
                 ? tab_usage_recorder::FOREGROUND_TAB_BACKGROUND_APP
                 : tab_usage_recorder::BACKGROUND_TAB_BACKGROUND_APP);

  UMA_HISTOGRAM_ENUMERATION(
      tab_usage_recorder::kRendererTerminationStateHistogram,
      static_cast<int>(web_state_state),
      static_cast<int>(tab_usage_recorder::TERMINATION_TAB_STATE_COUNT));
  if (!web_state_visible) {
    if (WebStateAlreadyEvicted(terminated_web_state)) {
      // A web state may get notified multiple times that it's been evicted.
      // To avoid double-counting, don't do any further processing if this
      // happens.
      return;
    }
    evicted_web_states_[terminated_web_state] =
        tab_usage_recorder::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];

  // 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(
      tab_usage_recorder::kRendererTerminationAliveRenderers,
      live_web_states_count);

  UMA_HISTOGRAM_COUNTS_100(
      tab_usage_recorder::kRendererTerminationTotalTabCount,
      web_state_list_->count());

  // Clear `termination_timestamps_` of timestamps older than
  // `kSecondsBeforeRendererTermination` ago.
  base::TimeDelta seconds_before =
      base::Seconds(tab_usage_recorder::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(
      tab_usage_recorder::kRendererTerminationRecentlyAliveRenderers,
      recently_live_web_states_count);

  ukm::SourceId source_id =
      ukm::GetSourceIdForWebStateDocument(terminated_web_state);
  if (source_id != ukm::kInvalidSourceId) {
    ukm::builders::IOS_RendererGone(source_id)
        .SetInForeground(web_state_state)
        .SetSawMemoryWarning(saw_memory_warning)
        .SetAliveRendererCount(live_web_states_count)
        .SetAliveRecentlyRendererCount(recently_live_web_states_count)
        .Record(ukm::UkmRecorder::Get());
  }
}

void TabUsageRecorderBrowserAgent::AppDidEnterBackground() {
  base::TimeTicks time_now = base::TimeTicks::Now();
  LOCAL_HISTOGRAM_TIMES(tab_usage_recorder::kTimeAfterLastRestore,
                        time_now - restore_start_time_);
  if (evicted_web_state_ &&
      evicted_web_state_reload_start_time_ != base::TimeTicks()) {
    ResetEvictedTab();
  }
}

void TabUsageRecorderBrowserAgent::AppWillEnterForeground() {
  restore_start_time_ = base::TimeTicks::Now();
}

void TabUsageRecorderBrowserAgent::ResetPageLoads() {
  page_loads_ = 0;
}

int TabUsageRecorderBrowserAgent::EvictedTabsMapSize() {
  return evicted_web_states_.size();
}

void TabUsageRecorderBrowserAgent::ResetAll() {
  ResetEvictedTab();
  ResetPageLoads();
  evicted_web_states_.clear();
}

void TabUsageRecorderBrowserAgent::ResetEvictedTab() {
  evicted_web_state_ = nullptr;
  evicted_web_state_state_ = tab_usage_recorder::IN_MEMORY;
  evicted_web_state_reload_start_time_ = base::TimeTicks();
}

bool TabUsageRecorderBrowserAgent::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.
  return web_state->GetVisibleURL().SchemeIs(kChromeUIScheme);
}

bool TabUsageRecorderBrowserAgent::WebStateAlreadyEvicted(
    web::WebState* web_state) {
  auto iter = evicted_web_states_.find(web_state);
  return iter != evicted_web_states_.end();
}

tab_usage_recorder::TabStateWhenSelected
TabUsageRecorderBrowserAgent::ExtractWebStateState(web::WebState* web_state) {
  if (!web_state->IsEvicted())
    return tab_usage_recorder::IN_MEMORY;

  auto iter = evicted_web_states_.find(web_state);
  if (iter != evicted_web_states_.end()) {
    tab_usage_recorder::TabStateWhenSelected web_state_state = iter->second;
    evicted_web_states_.erase(iter);
    return web_state_state;
  }

  return tab_usage_recorder::EVICTED;
}

void TabUsageRecorderBrowserAgent::RecordRestoreStartTime() {
  base::TimeTicks time_now = base::TimeTicks::Now();
  // Record the time delta since the last eviction reload was seen.
  LOCAL_HISTOGRAM_TIMES(tab_usage_recorder::kTimeBetweenRestores,
                        time_now - restore_start_time_);
  restore_start_time_ = time_now;
  evicted_web_state_reload_start_time_ = time_now;
}

int TabUsageRecorderBrowserAgent::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 TabUsageRecorderBrowserAgent::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);
}

bool TabUsageRecorderBrowserAgent::IsTransitionBetweenDesktopAndMobileUserAgent(
    web::UserAgentType agent_type,
    web::UserAgentType other_agent_type) {
  if (agent_type == web::UserAgentType::NONE)
    return false;

  if (other_agent_type == web::UserAgentType::NONE)
    return false;

  return agent_type != other_agent_type;
}

bool TabUsageRecorderBrowserAgent::ShouldRecordPageLoadStartForNavigation(
    web::NavigationContext* navigation) {
  web::NavigationManager* navigation_manager =
      navigation->GetWebState()->GetNavigationManager();

  web::NavigationItem* last_committed_item =
      navigation_manager->GetLastCommittedItem();
  if (!last_committed_item) {
    // Opening a child window and loading URL there.
    // http://crbug.com/773160
    return false;
  }

  web::NavigationItem* pending_item = navigation_manager->GetPendingItem();
  if (pending_item) {
    if (IsTransitionBetweenDesktopAndMobileUserAgent(
            pending_item->GetUserAgentType(),
            last_committed_item->GetUserAgentType())) {
      // Switching between Desktop and Mobile user agent.
      return false;
    }
  }

  ui::PageTransition transition = navigation->GetPageTransition();
  if (!ui::PageTransitionIsNewNavigation(transition)) {
    // Back/forward navigation or reload.
    return false;
  }

  if ((transition & ui::PAGE_TRANSITION_CLIENT_REDIRECT) != 0) {
    // Client redirect.
    return false;
  }

  static const ui::PageTransition kRecordedPageTransitionTypes[] = {
      ui::PAGE_TRANSITION_TYPED,
      ui::PAGE_TRANSITION_LINK,
      ui::PAGE_TRANSITION_GENERATED,
      ui::PAGE_TRANSITION_AUTO_BOOKMARK,
      ui::PAGE_TRANSITION_FORM_SUBMIT,
      ui::PAGE_TRANSITION_KEYWORD,
      ui::PAGE_TRANSITION_KEYWORD_GENERATED,
  };

  for (size_t i = 0; i < std::size(kRecordedPageTransitionTypes); ++i) {
    const ui::PageTransition recorded_type = kRecordedPageTransitionTypes[i];
    if (ui::PageTransitionCoreTypeIs(transition, recorded_type)) {
      return true;
    }
  }

  return false;
}

#pragma mark - WebStateObserver

void TabUsageRecorderBrowserAgent::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (PageTransitionCoreTypeIs(navigation_context->GetPageTransition(),
                               ui::PAGE_TRANSITION_RELOAD)) {
    RecordReload(web_state);
  }
  if (ShouldRecordPageLoadStartForNavigation(navigation_context)) {
    RecordPageLoadStart(web_state);
  }
}

void TabUsageRecorderBrowserAgent::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  RecordPageLoadDone(web_state);
}

void TabUsageRecorderBrowserAgent::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 TabUsageRecorderBrowserAgent::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_IN_MIGRATION();
}

#pragma mark - WebStateListObserver

void TabUsageRecorderBrowserAgent::WebStateListDidChange(
    WebStateList* web_state_list,
    const WebStateListChange& change,
    const WebStateListStatus& status) {
  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // The activation is handled after this switch statement.
      break;
    case WebStateListChange::Type::kDetach: {
      const WebStateListChangeDetach& detach_change =
          change.As<WebStateListChangeDetach>();
      OnWebStateDestroyed(detach_change.detached_web_state());
      break;
    }
    case WebStateListChange::Type::kMove:
      // Do nothing when a WebState is moved.
      break;
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replace_change =
          change.As<WebStateListChangeReplace>();
      OnWebStateDestroyed(replace_change.replaced_web_state());
      replace_change.inserted_web_state()->AddObserver(this);
      break;
    }
    case WebStateListChange::Type::kInsert: {
      const WebStateListChangeInsert& insert_change =
          change.As<WebStateListChangeInsert>();
      web::WebState* inserted_web_state = insert_change.inserted_web_state();
      if (status.active_web_state_change()) {
        web_state_created_selected_ = inserted_web_state;
      }

      inserted_web_state->AddObserver(this);
      break;
    }
    case WebStateListChange::Type::kGroupCreate:
      // Do nothing when a group is created.
      break;
    case WebStateListChange::Type::kGroupVisualDataUpdate:
      // Do nothing when a tab group's visual data are updated.
      break;
    case WebStateListChange::Type::kGroupMove:
      // Do nothing when a tab group is moved.
      break;
    case WebStateListChange::Type::kGroupDelete:
      // Do nothing when a group is deleted.
      break;
  }

  if (status.active_web_state_change() &&
      change.type() != WebStateListChange::Type::kReplace) {
    RecordTabSwitched(status.old_active_web_state, status.new_active_web_state);
  }
}

#pragma mark - SessionRestorationObserver

void TabUsageRecorderBrowserAgent::WillStartSessionRestoration(
    Browser* browser) {
  // Nothing to do.
}

void TabUsageRecorderBrowserAgent::SessionRestorationFinished(
    Browser* browser,
    const std::vector<web::WebState*>& restored_web_states) {
  // Ignore the event if it does not correspond to the browser this
  // object is bound to (which can happen with the optimised session
  // storage code).
  if (browser->GetWebStateList() != web_state_list_) {
    return;
  }

  InitialRestoredTabs(web_state_list_->GetActiveWebState(),
                      restored_web_states);
}