chromium/ios/chrome/browser/prerender/model/preload_controller.mm

// Copyright 2012 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/prerender/model/preload_controller.h"

#import <UIKit/UIKit.h>

#import "base/check_op.h"
#import "base/ios/device_util.h"
#import "base/metrics/field_trial.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_service.h"
#import "components/signin/ios/browser/account_consistency_service.h"
#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h"
#import "ios/chrome/browser/crash_report/model/crash_report_helper.h"
#import "ios/chrome/browser/download/model/mime_type_util.h"
#import "ios/chrome/browser/history/model/history_tab_helper.h"
#import "ios/chrome/browser/itunes_urls/model/itunes_urls_handler_tab_helper.h"
#import "ios/chrome/browser/prerender/model/preload_controller_delegate.h"
#import "ios/chrome/browser/prerender/model/prerender_pref.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/signin/model/account_consistency_service_factory.h"
#import "ios/chrome/browser/supervised_user/model/supervised_user_capabilities.h"
#import "ios/chrome/browser/tabs/model/tab_helper_util.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/web_state_policy_decider_bridge.h"
#import "ios/web/public/thread/web_thread.h"
#import "ios/web/public/ui/java_script_dialog_presenter.h"
#import "ios/web/public/web_client.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/page_transition_types.h"

using web::WebStatePolicyDecider;

// Protocol used to cancel a scheduled preload request.
@protocol PreloadCancelling <NSObject>

// Schedules the current prerender to be cancelled during the next run of the
// event loop.
- (void)schedulePrerenderCancel;

@end

namespace {

// PrerenderFinalStatus values are used in the "Prerender.FinalStatus" histogram
// and new values needs to be kept in sync with histogram.xml.
enum PrerenderFinalStatus {
  PRERENDER_FINAL_STATUS_USED = 0,
  PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED = 12,
  PRERENDER_FINAL_STATUS_CANCELLED = 32,
  PRERENDER_FINAL_STATUS_MAX = 52,
};

// Delay before starting to prerender a URL.
const NSTimeInterval kPrerenderDelay = 0.5;

// The finch experiment to turn off prerendering as a field trial.
const char kTabEvictionFieldTrialName[] = "TabEviction";
// The associated group.
const char kPrerenderTabEvictionTrialGroup[] = "NoPrerendering";
// The name of the histogram for recording final status (e.g. used/cancelled)
// of prerender requests.
const char kPrerenderFinalStatusHistogramName[] = "Prerender.FinalStatus";
// The name of the histogram for recording time until a successful prerender.
const char kPrerenderPrerenderTimeSaved[] = "Prerender.PrerenderTimeSaved";
// Histogram to record that the load was complete when the prerender was used.
// Not recorded if the pre-render isn't used.
const char kPrerenderLoadComplete[] = "Prerender.PrerenderLoadComplete";

// Is this install selected for this particular experiment.
bool IsPrerenderTabEvictionExperimentalGroup() {
  base::FieldTrial* trial =
      base::FieldTrialList::Find(kTabEvictionFieldTrialName);
  return trial && trial->group_name() == kPrerenderTabEvictionTrialGroup;
}

// Returns true if the primary account is subject to parental controls and the
// URL filtering control has been enabled.
bool IsSubjectToParentalControls(ChromeBrowserState* browserState) {
  return browserState &&
         supervised_user::IsSubjectToParentalControls(browserState);
}

// Returns whether `url` can be prerendered.
bool CanPrerenderURL(const GURL& url) {
  // Prerendering is only enabled for http and https URLs.
  return url.is_valid() &&
         (url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme));
}

// Object used to schedule prerenders.
class PrerenderRequest {
 public:
  PrerenderRequest() {}
  PrerenderRequest(const GURL& url,
                   ui::PageTransition transition,
                   const web::Referrer& referrer)
      : url_(url), transition_(transition), referrer_(referrer) {}

  const GURL& url() const { return url_; }
  ui::PageTransition transition() const { return transition_; }
  const web::Referrer referrer() const { return referrer_; }

 private:
  const GURL url_;
  const ui::PageTransition transition_ = ui::PAGE_TRANSITION_LINK;
  const web::Referrer referrer_;
};

// A no-op JavaScriptDialogPresenter that cancels prerendering when the
// prerendered page attempts to show dialogs.
class PreloadJavaScriptDialogPresenter : public web::JavaScriptDialogPresenter {
 public:
  explicit PreloadJavaScriptDialogPresenter(
      id<PreloadCancelling> cancel_handler)
      : cancel_handler_(cancel_handler) {
    DCHECK(cancel_handler_);
  }

  // web::JavaScriptDialogPresenter:
  void RunJavaScriptAlertDialog(web::WebState* web_state,
                                const GURL& origin_url,
                                NSString* message_text,
                                base::OnceClosure callback) override {
    std::move(callback).Run();
    [cancel_handler_ schedulePrerenderCancel];
  }

  void RunJavaScriptConfirmDialog(
      web::WebState* web_state,
      const GURL& origin_url,
      NSString* message_text,
      base::OnceCallback<void(bool success)> callback) override {
    std::move(callback).Run(/*success=*/false);
    [cancel_handler_ schedulePrerenderCancel];
  }

  void RunJavaScriptPromptDialog(
      web::WebState* web_state,
      const GURL& origin_url,
      NSString* message_text,
      NSString* default_prompt_text,
      base::OnceCallback<void(NSString* user_input)> callback) override {
    std::move(callback).Run(/*user_input=*/nil);
    [cancel_handler_ schedulePrerenderCancel];
  }

  void CancelDialogs(web::WebState* web_state) override {}

 private:
  __weak id<PreloadCancelling> cancel_handler_ = nil;
};

class PreloadManageAccountsDelegate : public ManageAccountsDelegate {
 public:
  explicit PreloadManageAccountsDelegate(id<PreloadCancelling> canceler)
      : canceler_(canceler) {}
  ~PreloadManageAccountsDelegate() override {}

  void OnRestoreGaiaCookies() override { [canceler_ schedulePrerenderCancel]; }
  void OnManageAccounts() override { [canceler_ schedulePrerenderCancel]; }
  void OnAddAccount() override { [canceler_ schedulePrerenderCancel]; }
  void OnShowConsistencyPromo(const GURL& url,
                              web::WebState* webState) override {
    [canceler_ schedulePrerenderCancel];
  }
  void OnGoIncognito(const GURL& url) override {
    [canceler_ schedulePrerenderCancel];
  }

 private:
  __weak id<PreloadCancelling> canceler_;
};

// Maximum time to let a cancelled webState attempt to finish restore.
static const size_t kMaximumCancelledWebStateDelay = 2;

// Helper function to destroy a pre-rendering WebState. This is a free function
// so that the code does not accidently try to access to PreloadController's
// _webState ivar (which has been set to null by the time this function is
// called).
void DestroyPrerenderingWebState(std::unique_ptr<web::WebState> web_state) {
  // Preload appears to trigger an edge-case crash in WebKit when a restore is
  // triggered and cancelled before it can complete.  This isn't specific to
  // preload, but is very easy to trigger in preload.  As a speculative fix, if
  // a preload is in restore, don't destroy it until after restore is complete.
  // This logic should really belong in WebState itself, so any attempt to
  // destroy a WebState during restore will trigger this logic.  Even better,
  // this edge case crash should be fixed in WebKit:
  //     https://bugs.webkit.org/show_bug.cgi?id=217440.
  // The crash in WebKit appears to be related to IPC throttling.  Session
  // restore can create a large number of IPC calls, which can then be
  // throttled.  It seems if the WKWebView is destroyed with this backlog of
  // IPC calls, sometimes WebKit crashes.
  // See crbug.com/1032928 for an explanation for how to trigger this crash.
  // Note the timer should only be called if for some reason session restoration
  // fails to complete -- thus preventing a WebState leak.
  if (!web_state->GetNavigationManager()->IsRestoreSessionInProgress()) {
    web_state.reset();
    return;
  }

  __block auto reset_timer = std::make_unique<base::OneShotTimer>();
  __block std::unique_ptr<web::WebState> block_web_state = std::move(web_state);

  auto reset_block = ^{
    if (block_web_state) {
      block_web_state.reset();
    }

    if (!reset_timer) {
      return;
    }

    reset_timer->Stop();
    reset_timer.reset();
  };

  reset_timer->Start(FROM_HERE, base::Seconds(kMaximumCancelledWebStateDelay),
                     base::BindOnce(reset_block));

  block_web_state->GetNavigationManager()->AddRestoreCompletionCallback(
      base::BindOnce(^{
        dispatch_async(dispatch_get_main_queue(), reset_block);
      }));
}

}  // namespace

@interface PreloadController () <CRConnectionTypeObserverBridge,
                                 CRWWebStateDelegate,
                                 CRWWebStateObserver,
                                 CRWWebStatePolicyDecider,
                                 PrefObserverDelegate,
                                 PreloadCancelling> {
  std::unique_ptr<web::WebStateDelegateBridge> _webStateDelegate;
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
  std::unique_ptr<PrefObserverBridge> _observerBridge;
  std::unique_ptr<ConnectionTypeObserverBridge> _connectionTypeObserver;
  std::unique_ptr<web::WebStatePolicyDeciderBridge> _policyDeciderBridge;

  // The WebState used for prerendering.
  std::unique_ptr<web::WebState> _webState;

  // The scheduled request.
  std::unique_ptr<PrerenderRequest> _scheduledRequest;

  // Registrar for pref changes notifications.
  PrefChangeRegistrar _prefChangeRegistrar;

  // The dialog presenter.
  std::unique_ptr<web::JavaScriptDialogPresenter> _dialogPresenter;

  // A weak pointer to the webState that will be replaced with the prerendered
  // one. This is needed by `startPrerender` to build the new webstate with the
  // same sessions.
  base::WeakPtr<web::WebState> _webStateToReplace;

  std::unique_ptr<PreloadManageAccountsDelegate> _manageAccountsDelegate;
}

// The ChromeBrowserState passed on initialization.
@property(nonatomic) ChromeBrowserState* browserState;

// Redefine property as readwrite.  The URL that is prerendered in `_webState`.
// This can be different from the value returned by WebState last committed
// navigation item, for example in cases where there was a redirect.
//
// When choosing whether or not to use a prerendered Tab,
// BrowserViewController compares the URL being loaded by the omnibox with the
// URL of the prerendered Tab.  Comparing against the Tab's currently URL
// could return false negatives in cases of redirect, hence the need to store
// the originally prerendered URL.
@property(nonatomic, readwrite, assign) GURL prerenderedURL;

// The URL in the currently scheduled prerender request, or an empty one if
// there is no prerender scheduled.
@property(nonatomic, readonly) const GURL& scheduledURL;

// Network prediction settings.
@property(nonatomic)
    prerender_prefs::NetworkPredictionSetting networkPredictionSetting;

// Whether or not the current connection is using WWAN.
@property(nonatomic, assign) BOOL isOnCellularNetwork;

// Tracks the time of the last attempt to load a prerender URL. Used for UMA
// reporting of load durations.
@property(nonatomic) base::TimeTicks startTime;

// Whether the load was completed or not.
@property(nonatomic, assign) BOOL loadCompleted;
// The time between the start of the load and the completion (only valid if the
// load completed).
@property(nonatomic) base::TimeDelta completionTime;

// Called to start any scheduled prerendering requests.
- (void)startPrerender;

// Destroys the preview Tab and resets `prerenderURL_` to the empty URL.
- (void)destroyPreviewContents;

// Removes any scheduled prerender requests and resets `scheduledURL` to the
// empty URL.
- (void)removeScheduledPrerenderRequests;

// Records metric on a successful prerender.
- (void)recordReleaseMetrics;

@end

@implementation PreloadController

- (instancetype)initWithBrowserState:(ChromeBrowserState*)browserState {
  DCHECK(browserState);
  DCHECK_CURRENTLY_ON(web::WebThread::UI);
  if ((self = [super init])) {
    _browserState = browserState;
    _networkPredictionSetting =
        static_cast<prerender_prefs::NetworkPredictionSetting>(
            _browserState->GetPrefs()->GetInteger(
                prefs::kNetworkPredictionSetting));
    _isOnCellularNetwork = net::NetworkChangeNotifier::IsConnectionCellular(
        net::NetworkChangeNotifier::GetConnectionType());
    _webStateDelegate = std::make_unique<web::WebStateDelegateBridge>(self);
    _webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
    _observerBridge = std::make_unique<PrefObserverBridge>(self);
    _prefChangeRegistrar.Init(_browserState->GetPrefs());
    _observerBridge->ObserveChangesForPreference(
        prefs::kNetworkPredictionSetting, &_prefChangeRegistrar);
    _dialogPresenter = std::make_unique<PreloadJavaScriptDialogPresenter>(self);
    _manageAccountsDelegate =
        std::make_unique<PreloadManageAccountsDelegate>(self);
    if (_networkPredictionSetting ==
        prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly) {
      _connectionTypeObserver =
          std::make_unique<ConnectionTypeObserverBridge>(self);
    }
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(didReceiveMemoryWarning)
               name:UIApplicationDidReceiveMemoryWarningNotification
             object:nil];
  }
  return self;
}

- (void)dealloc {
  [self cancelPrerender];
}

#pragma mark - Accessors

- (const GURL&)scheduledURL {
  return _scheduledRequest ? _scheduledRequest->url() : GURL::EmptyGURL();
}

- (BOOL)isEnabled {
  DCHECK_CURRENTLY_ON(web::WebThread::UI);

  if (IsPrerenderTabEvictionExperimentalGroup() ||
      ios::device_util::IsSingleCoreDevice() ||
      !ios::device_util::RamIsAtLeast512Mb() ||
      net::NetworkChangeNotifier::IsOffline() ||
      IsSubjectToParentalControls(_browserState)) {
    return false;
  }

  switch (self.networkPredictionSetting) {
    case prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly: {
      return !self.isOnCellularNetwork;
    }

    case prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular: {
      return true;
    }

    case prerender_prefs::NetworkPredictionSetting::kDisabled: {
      return false;
    }
  }
}

- (void)setLoadCompleted:(BOOL)loadCompleted {
  if (_loadCompleted == loadCompleted) {
    return;
  }

  _loadCompleted = loadCompleted;

  if (loadCompleted) {
    self.completionTime = base::TimeTicks::Now() - self.startTime;
  } else {
    self.completionTime = base::TimeDelta();
  }
}

#pragma mark - Public

- (void)browserStateDestroyed {
  [self cancelPrerender];
  _connectionTypeObserver.reset();
}

- (void)prerenderURL:(const GURL&)url
            referrer:(const web::Referrer&)referrer
          transition:(ui::PageTransition)transition
     currentWebState:(web::WebState*)currentWebState
         immediately:(BOOL)immediately {
  // TODO(crbug.com/40534385): If CanPrerenderURL() returns false, should we
  // cancel any scheduled prerender requests?
  if (!self.enabled || !CanPrerenderURL(url)) {
    return;
  }

  // Ignore this request if there is already a scheduled request for the same
  // URL; or, if there is no scheduled request, but the currently prerendered
  // page matches this URL.
  if (url == self.scheduledURL ||
      (self.scheduledURL.is_empty() && url == self.prerenderedURL)) {
    return;
  }

  [self removeScheduledPrerenderRequests];
  _webStateToReplace = currentWebState->GetWeakPtr();
  _scheduledRequest =
      std::make_unique<PrerenderRequest>(url, transition, referrer);

  NSTimeInterval delay = immediately ? 0.0 : kPrerenderDelay;
  [self performSelector:@selector(startPrerender)
             withObject:nil
             afterDelay:delay];
}

- (void)cancelPrerender {
  [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED];
}

- (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason {
  [self removeScheduledPrerenderRequests];
  [self destroyPreviewContentsForReason:reason];
}

- (BOOL)isWebStatePrerendered:(web::WebState*)webState {
  return webState && _webState.get() == webState;
}

- (std::unique_ptr<web::WebState>)releasePrerenderContents {
  if (!_webState ||
      _webState->GetNavigationManager()->IsRestoreSessionInProgress()) {
    return nullptr;
  }

  [self recordReleaseMetrics];
  [self removeScheduledPrerenderRequests];

  // Use the helper function to properly release the web::WebState.
  std::unique_ptr<web::WebState> webState =
      [self releasePrerenderContentsInternal];

  // The WebState will be converted to a proper tab. Record navigations that
  // happened during pre-rendering to the HistoryService.
  HistoryTabHelper::FromWebState(webState.get())
      ->SetDelayHistoryServiceNotification(false);

  return webState;
}

#pragma mark - Internal

// Helper function that return ownership of the internal web::WebState instance
// disconnecting the observers attached to it for preloading, ... Needs to be
// called before destroying the WebState or before converting it to a tab.
- (std::unique_ptr<web::WebState>)releasePrerenderContentsInternal {
  DCHECK(_webState);

  self.prerenderedURL = GURL();
  self.startTime = base::TimeTicks();
  self.loadCompleted = NO;

  // Move the pre-rendered WebState to a local variable so that it will no
  // longer be considered as pre-rendering (otherwise tab helpers may early
  // exist when invoked).
  std::unique_ptr<web::WebState> webState = std::move(_webState);
  DCHECK(![self isWebStatePrerendered:webState.get()]);

  webState->RemoveObserver(_webStateObserver.get());
  crash_report_helper::StopMonitoringURLsForPreloadWebState(webState.get());
  webState->SetDelegate(nullptr);
  _policyDeciderBridge.reset();

  if (AccountConsistencyService* accountConsistencyService =
          ios::AccountConsistencyServiceFactory::GetForBrowserState(
              self.browserState)) {
    accountConsistencyService->RemoveWebStateHandler(webState.get());
  }

  return webState;
}

#pragma mark - CRConnectionTypeObserverBridge

- (void)connectionTypeChanged:(net::NetworkChangeNotifier::ConnectionType)type {
  DCHECK_CURRENTLY_ON(web::WebThread::UI);
  self.isOnCellularNetwork =
      net::NetworkChangeNotifier::IsConnectionCellular(type);
  if (self.networkPredictionSetting ==
          prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly &&
      self.isOnCellularNetwork) {
    [self cancelPrerender];
  }
}

#pragma mark - CRWWebStateDelegate

- (web::WebState*)webState:(web::WebState*)webState
    createNewWebStateForURL:(const GURL&)URL
                  openerURL:(const GURL&)openerURL
            initiatedByUser:(BOOL)initiatedByUser {
  DCHECK([self isWebStatePrerendered:webState]);
  [self schedulePrerenderCancel];
  return nil;
}

- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
    (web::WebState*)webState {
  DCHECK([self isWebStatePrerendered:webState]);
  return _dialogPresenter.get();
}

- (void)webState:(web::WebState*)webState
    didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace
                      proposedCredential:(NSURLCredential*)proposedCredential
                       completionHandler:(void (^)(NSString* username,
                                                   NSString* password))handler {
  DCHECK([self isWebStatePrerendered:webState]);
  [self schedulePrerenderCancel];
  if (handler) {
    handler(nil, nil);
  }
}

- (UIView*)webViewContainerForWebState:(web::WebState*)webState {
  return [self.delegate webViewContainer];
}

#pragma mark - CRWWebStateObserver

- (void)webState:(web::WebState*)webState
    didFinishNavigation:(web::NavigationContext*)navigation {
  DCHECK_EQ(webState, _webState.get());
  if ([self shouldCancelPreloadForMimeType:webState->GetContentsMimeType()]) {
    [self schedulePrerenderCancel];
  }
}

- (void)webState:(web::WebState*)webState
    didLoadPageWithSuccess:(BOOL)loadSuccess {
  DCHECK_EQ(webState, _webState.get());
  // The load should have been cancelled when the navigation finishes, but this
  // makes sure that we didn't miss one.
  if ([self shouldCancelPreloadForMimeType:webState->GetContentsMimeType()]) {
    [self schedulePrerenderCancel];
  } else if (loadSuccess) {
    self.loadCompleted = YES;
  }
}

#pragma mark - CRWWebStatePolicyDecider

- (void)shouldAllowRequest:(NSURLRequest*)request
               requestInfo:(WebStatePolicyDecider::RequestInfo)requestInfo
           decisionHandler:(PolicyDecisionHandler)decisionHandler {
  GURL requestURL = net::GURLWithNSURL(request.URL);
  // Don't allow preloading for requests that are handled by opening another
  // application or by presenting a native UI.
  if (AppLauncherTabHelper::IsAppUrl(requestURL) ||
      ITunesUrlsHandlerTabHelper::CanHandleUrl(requestURL)) {
    [self schedulePrerenderCancel];
    decisionHandler(WebStatePolicyDecider::PolicyDecision::Cancel());
    return;
  }
  decisionHandler(WebStatePolicyDecider::PolicyDecision::Allow());
}

#pragma mark - PrefObserverDelegate

- (void)onPreferenceChanged:(const std::string&)preferenceName {
  if (preferenceName == prefs::kNetworkPredictionSetting) {
    DCHECK_CURRENTLY_ON(web::WebThread::UI);
    // The logic is simpler if both preferences changes are handled equally.
    self.networkPredictionSetting =
        static_cast<prerender_prefs::NetworkPredictionSetting>(
            self.browserState->GetPrefs()->GetInteger(
                prefs::kNetworkPredictionSetting));

    switch (self.networkPredictionSetting) {
      case prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly: {
        if (!_connectionTypeObserver.get()) {
          self.isOnCellularNetwork =
              net::NetworkChangeNotifier::IsConnectionCellular(
                  net::NetworkChangeNotifier::GetConnectionType());
          _connectionTypeObserver =
              std::make_unique<ConnectionTypeObserverBridge>(self);
        }
        if (self.isOnCellularNetwork) {
          [self cancelPrerender];
        }
        break;
      }

      case prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular: {
        _connectionTypeObserver.reset();
        break;
      }

      case prerender_prefs::NetworkPredictionSetting::kDisabled: {
        [self cancelPrerender];
        _connectionTypeObserver.reset();
        break;
      }
    }
  }
}

#pragma mark - PreloadCancelling

- (void)schedulePrerenderCancel {
  // TODO(crbug.com/41005198): Instead of cancelling the prerender, should we
  // mark it as failed instead?  That way, subsequent prerender requests for the
  // same URL will not kick off new prerenders.
  [self removeScheduledPrerenderRequests];
  [self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0];
}

#pragma mark - Cancellation Helpers

- (BOOL)shouldCancelPreloadForMimeType:(std::string)mimeType {
  // Cancel prerendering if response is "application/octet-stream". It can be a
  // video file which should not be played from preload tab. See issue at
  // http://crbug.com/436813 for more details.
  // On iOS 13, PDF are getting focused when loaded, preventing the user from
  // typing in the omnibox. See crbug.com/1017352.
  return mimeType == kBinaryDataMimeType || mimeType == "application/pdf";
}

- (void)removeScheduledPrerenderRequests {
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
  _scheduledRequest = nullptr;
  _webStateToReplace.reset();
}

#pragma mark - Prerender Helpers

- (void)startPrerender {
  // Destroy any existing prerenders before starting a new one.
  [self destroyPreviewContents];
  self.prerenderedURL = self.scheduledURL;
  std::unique_ptr<PrerenderRequest> request = std::move(_scheduledRequest);

  web::WebState* webStateToReplace = _webStateToReplace.get();
  _webStateToReplace.reset();

  // TODO(crbug.com/40726702): The correct way is to always get the
  // webStateToReplace from the delegate. however this is not possible because
  // there is only one delegate per browser state.
  if (!webStateToReplace) {
    webStateToReplace = [self.delegate webStateToReplace];
  }

  if (!self.prerenderedURL.is_valid() || !webStateToReplace) {
    [self destroyPreviewContents];
    return;
  }

  // To avoid losing the navigation history when the user navigates to the
  // pre-rendered tab, clone the tab that will be replaced, and start the
  // pre-rendered navigation in the new tab.
  _webState = webStateToReplace->Clone();

  // Add the preload controller as a policyDecider before other tab helpers, so
  // that it can block the navigation if needed before other policy deciders
  // execute thier side effects (eg. AppLauncherTabHelper launching app).
  _policyDeciderBridge =
      std::make_unique<web::WebStatePolicyDeciderBridge>(_webState.get(), self);
  AttachTabHelpers(_webState.get(), TabHelperFilter::kPrerender);

  _webState->SetDelegate(_webStateDelegate.get());
  _webState->AddObserver(_webStateObserver.get());
  crash_report_helper::MonitorURLsForPreloadWebState(_webState.get());
  _webState->SetWebUsageEnabled(true);

  if (AccountConsistencyService* accountConsistencyService =
          ios::AccountConsistencyServiceFactory::GetForBrowserState(
              self.browserState)) {
    accountConsistencyService->SetWebStateHandler(
        _webState.get(), _manageAccountsDelegate.get());
  }

  HistoryTabHelper::FromWebState(_webState.get())
      ->SetDelayHistoryServiceNotification(true);

  web::NavigationManager::WebLoadParams loadParams(self.prerenderedURL);
  loadParams.referrer = request->referrer();
  loadParams.transition_type = request->transition();
  _webState->SetKeepRenderProcessAlive(true);
  _webState->GetNavigationManager()->LoadURLWithParams(loadParams);

  // LoadIfNecessary is needed because the view is not created (but needed) when
  // loading the page. TODO(crbug.com/41309809): Remove this call.
  _webState->GetNavigationManager()->LoadIfNecessary();

  self.startTime = base::TimeTicks::Now();
  self.loadCompleted = NO;
}

#pragma mark - Teardown Helpers

- (void)destroyPreviewContents {
  [self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED];
}

- (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason {
  if (!_webState) {
    return;
  }

  UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason,
                            PRERENDER_FINAL_STATUS_MAX);

  // Use the helper function to properly destroy the WebState.
  DestroyPrerenderingWebState([self releasePrerenderContentsInternal]);
}

#pragma mark - Notification Helpers

- (void)didReceiveMemoryWarning {
  [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED];
}

#pragma mark - Metrics Helpers

- (void)recordReleaseMetrics {
  UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName,
                            PRERENDER_FINAL_STATUS_USED,
                            PRERENDER_FINAL_STATUS_MAX);

  UMA_HISTOGRAM_BOOLEAN(kPrerenderLoadComplete, self.loadCompleted);

  if (self.loadCompleted) {
    DCHECK_NE(base::TimeDelta(), self.completionTime);
    UMA_HISTOGRAM_TIMES(kPrerenderPrerenderTimeSaved, self.completionTime);
  } else {
    DCHECK_NE(base::TimeTicks(), self.startTime);
    UMA_HISTOGRAM_TIMES(kPrerenderPrerenderTimeSaved,
                        base::TimeTicks::Now() - self.startTime);
  }
}

@end