| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/preloading/prerender/prerender_host_registry.h" |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/memory_pressure_monitor.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/observer_list.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/devtools/render_frame_devtools_agent_host.h" |
| #include "content/browser/preloading/preloading_data_impl.h" |
| #include "content/browser/preloading/preloading_trigger_type_impl.h" |
| #include "content/browser/preloading/prerender/devtools_prerender_attempt.h" |
| #include "content/browser/preloading/prerender/prerender_features.h" |
| #include "content/browser/preloading/prerender/prerender_final_status.h" |
| #include "content/browser/preloading/prerender/prerender_metrics.h" |
| #include "content/browser/preloading/prerender/prerender_navigation_utils.h" |
| #include "content/browser/preloading/prerender/prerender_new_tab_handle.h" |
| #include "content/browser/preloading/speculation_rules/speculation_rules_util.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/frame.mojom.h" |
| #include "content/public/browser/client_hints_controller_delegate.h" |
| #include "content/public/browser/preloading.h" |
| #include "content/public/browser/preloading_data.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/visibility.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_delegate.h" |
| #include "net/base/load_flags.h" |
| #include "services/network/public/cpp/network_quality_tracker.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "third_party/abseil-cpp/absl/cleanup/cleanup.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| bool IsBackground(Visibility visibility) { |
| // PrerenderHostRegistry treats HIDDEN and OCCLUDED as background. |
| switch (visibility) { |
| case Visibility::HIDDEN: |
| case Visibility::OCCLUDED: |
| return true; |
| case Visibility::VISIBLE: |
| return false; |
| } |
| } |
| |
| // Returns true when it is allowed to activate a prerendered page in a |
| // background tab. |
| bool IsAllowedToActivateInBackgroundForTesting() { |
| // Now it is allowed to activate a prerendered page in a background only on |
| // macOS for running web platform tests. See comments on the flag definition |
| // for more details. |
| #if BUILDFLAG(IS_MAC) |
| if (base::FeatureList::IsEnabled( |
| features::kPrerender2AllowActivationInBackground)) { |
| return true; |
| } |
| #endif |
| return false; |
| } |
| |
| bool DeviceHasEnoughMemoryForPrerender() { |
| // This method disallows prerendering on low-end devices if the |
| // kPrerender2MemoryControls feature is enabled. |
| if (!base::FeatureList::IsEnabled(blink::features::kPrerender2MemoryControls)) |
| return true; |
| |
| // On Android, Prerender2 is only enabled for 2GB+ high memory devices. The |
| // default threshold value is set to 1700 MB to account for all 2GB devices |
| // which report lower RAM due to carveouts. |
| // Previously used the same default threshold as the back/forward cache. See |
| // comments in DeviceHasEnoughMemoryForBackForwardCache(). |
| // TODO(crbug.com/40277975): experiment with 1200 MB threshold like |
| // back/forward cache. |
| static constexpr int kDefaultMemoryThresholdMb = |
| #if BUILDFLAG(IS_ANDROID) |
| 1700; |
| #else |
| 0; |
| #endif |
| |
| // The default is overridable by field trial param. |
| int memory_threshold_mb = base::GetFieldTrialParamByFeatureAsInt( |
| blink::features::kPrerender2MemoryControls, |
| blink::features::kPrerender2MemoryThresholdParamName, |
| kDefaultMemoryThresholdMb); |
| |
| return base::SysInfo::AmountOfPhysicalMemory().InMiB() > memory_threshold_mb; |
| } |
| |
| base::MemoryPressureListener::MemoryPressureLevel |
| GetCurrentMemoryPressureLevel() { |
| // Ignore the memory pressure event if the memory control is disabled. |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kPrerender2MemoryControls)) { |
| return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE; |
| } |
| |
| auto* monitor = base::MemoryPressureMonitor::Get(); |
| if (!monitor) { |
| return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE; |
| } |
| return monitor->GetCurrentPressureLevel( |
| base::MemoryPressureMonitorTag::kPrerenderHostRegistry); |
| } |
| |
| // Create a resource request for `back_url` that only checks whether the |
| // resource is in the HTTP cache. |
| std::unique_ptr<network::SimpleURLLoader> CreateHttpCacheQueryingResourceLoad( |
| const GURL& back_url) { |
| std::unique_ptr<network::ResourceRequest> request = |
| std::make_unique<network::ResourceRequest>(); |
| request->url = back_url; |
| request->load_flags = |
| net::LOAD_ONLY_FROM_CACHE | net::LOAD_SKIP_CACHE_VALIDATION; |
| if (base::FeatureList::IsEnabled( |
| blink::features::kAvoidTrustedParamsCopies)) { |
| request->trusted_params = network::ResourceRequest::TrustedParams(); |
| url::Origin origin = url::Origin::Create(back_url); |
| request->trusted_params->isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin)); |
| request->site_for_cookies = |
| request->trusted_params->isolation_info.site_for_cookies(); |
| } else { |
| url::Origin origin = url::Origin::Create(back_url); |
| net::IsolationInfo isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin)); |
| network::ResourceRequest::TrustedParams trusted_params; |
| trusted_params.isolation_info = isolation_info; |
| |
| request->trusted_params = trusted_params; |
| request->site_for_cookies = |
| trusted_params.isolation_info.site_for_cookies(); |
| } |
| request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| request->skip_service_worker = true; |
| request->do_not_prompt_for_login = true; |
| |
| CHECK(!request->SendsCookies()); |
| CHECK(!request->SavesCookies()); |
| constexpr net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("back_navigation_cache_query", |
| R"( |
| semantics { |
| sender: "Prerender" |
| description: |
| "This is not actually a network request. It is used internally by the browser to determine if the HTTP cache would be used if the user were to navigate back in session history. It only checks the cache and does not hit the network." |
| trigger: |
| "When the user performs an action that would suggest that they intend to navigate back soon. Examples include hovering the mouse over the back button and the start of a gestural back navigation." |
| user_data { |
| type: NONE |
| } |
| data: "None. The request doesn't hit the network." |
| destination: LOCAL |
| internal { |
| contacts { |
| email: "chrome-brapp-loading@chromium.org" |
| } |
| } |
| last_reviewed: "2023-03-24" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This is not controlled by a setting." |
| policy_exception_justification: "This is not a network request." |
| })"); |
| |
| return network::SimpleURLLoader::Create(std::move(request), |
| traffic_annotation); |
| } |
| |
| // Returns true if the given navigation is meant to be predicted by a predictor |
| // related to session history (e.g. hovering over the back button could have |
| // predicted the navigation). |
| bool IsNavigationInSessionHistoryPredictorDomain(NavigationHandle* handle) { |
| CHECK(handle->IsInPrimaryMainFrame()); |
| CHECK(!handle->IsSameDocument()); |
| |
| if (handle->IsRendererInitiated()) { |
| return false; |
| } |
| |
| // Note that currently the only predictors are for back navigations of a |
| // single step, however we still include all session history navigations in |
| // the domain. The preloading of back navigations could generalize to session |
| // history navigations of other offsets, but we haven't explored this due to |
| // the higher usage of the back button compared to the forward button or |
| // history menu. |
| if (!(handle->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK)) { |
| return false; |
| } |
| |
| if (handle->IsPost()) { |
| return false; |
| } |
| |
| if (handle->IsServedFromBackForwardCache()) { |
| return false; |
| } |
| |
| if (!handle->GetURL().SchemeIsHTTPOrHTTPS()) { |
| return false; |
| } |
| |
| // Note that even though the current predictors do not handle session history |
| // navigations that are same-site or which don't use the HTTP cache, they are |
| // still included in the domain. |
| return true; |
| } |
| |
| PreloadingEligibility ToEligibility(PrerenderFinalStatus status) { |
| switch (status) { |
| case PrerenderFinalStatus::kActivated: |
| case PrerenderFinalStatus::kDestroyed: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kLowEndDevice: |
| return PreloadingEligibility::kLowMemory; |
| case PrerenderFinalStatus::kInvalidSchemeRedirect: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kInvalidSchemeNavigation: |
| return PreloadingEligibility::kHttpOrHttpsOnly; |
| case PrerenderFinalStatus::kNavigationRequestBlockedByCsp: |
| case PrerenderFinalStatus::kMojoBinderPolicy: |
| case PrerenderFinalStatus::kRendererProcessCrashed: |
| case PrerenderFinalStatus::kRendererProcessKilled: |
| case PrerenderFinalStatus::kDownload: |
| case PrerenderFinalStatus::kTriggerDestroyed: |
| case PrerenderFinalStatus::kNavigationNotCommitted: |
| case PrerenderFinalStatus::kNavigationBadHttpStatus: |
| case PrerenderFinalStatus::kClientCertRequested: |
| case PrerenderFinalStatus::kNavigationRequestNetworkError: |
| case PrerenderFinalStatus::kCancelAllHostsForTesting: |
| case PrerenderFinalStatus::kDidFailLoad: |
| case PrerenderFinalStatus::kStop: |
| case PrerenderFinalStatus::kSslCertificateError: |
| case PrerenderFinalStatus::kLoginAuthRequested: |
| case PrerenderFinalStatus::kUaChangeRequiresReload: |
| case PrerenderFinalStatus::kBlockedByClient: |
| case PrerenderFinalStatus::kMixedContent: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kTriggerBackgrounded: |
| return PreloadingEligibility::kHidden; |
| case PrerenderFinalStatus::kMemoryLimitExceeded: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kDataSaverEnabled: |
| return PreloadingEligibility::kDataSaverEnabled; |
| case PrerenderFinalStatus::kTriggerUrlHasEffectiveUrl: |
| return PreloadingEligibility::kHasEffectiveUrl; |
| case PrerenderFinalStatus::kActivatedBeforeStarted: |
| case PrerenderFinalStatus::kInactivePageRestriction: |
| case PrerenderFinalStatus::kStartFailed: |
| case PrerenderFinalStatus::kTimeoutBackgrounded: |
| case PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation: |
| return PreloadingEligibility::kCrossOrigin; |
| case PrerenderFinalStatus:: |
| kSameSiteCrossOriginRedirectNotOptInInInitialNavigation: |
| case PrerenderFinalStatus:: |
| kSameSiteCrossOriginNavigationNotOptInInInitialNavigation: |
| case PrerenderFinalStatus::kActivationNavigationParameterMismatch: |
| case PrerenderFinalStatus::kActivatedInBackground: |
| case PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess: |
| case PrerenderFinalStatus::kTabClosedByUserGesture: |
| case PrerenderFinalStatus::kTabClosedWithoutUserGesture: |
| case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed: |
| case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled: |
| case PrerenderFinalStatus::kActivationFramePolicyNotCompatible: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kPreloadingDisabled: |
| return PreloadingEligibility::kPreloadingDisabled; |
| case PrerenderFinalStatus::kBatterySaverEnabled: |
| return PreloadingEligibility::kBatterySaverEnabled; |
| case PrerenderFinalStatus::kActivatedDuringMainFrameNavigation: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kPreloadingUnsupportedByWebContents: |
| return PreloadingEligibility::kPreloadingUnsupportedByWebContents; |
| case PrerenderFinalStatus::kCrossSiteRedirectInMainFrameNavigation: |
| case PrerenderFinalStatus::kCrossSiteNavigationInMainFrameNavigation: |
| case PrerenderFinalStatus:: |
| kSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation: |
| case PrerenderFinalStatus:: |
| kSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kMemoryPressureOnTrigger: |
| return PreloadingEligibility::kMemoryPressure; |
| case PrerenderFinalStatus::kMemoryPressureAfterTriggered: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kPrerenderingDisabledByDevTools: |
| return PreloadingEligibility::kPreloadingDisabledByDevTools; |
| case PrerenderFinalStatus::kSpeculationRuleRemoved: |
| case PrerenderFinalStatus::kActivatedWithAuxiliaryBrowsingContexts: |
| case PrerenderFinalStatus::kMaxNumOfRunningImmediatePrerendersExceeded: |
| case PrerenderFinalStatus::kMaxNumOfRunningNonImmediatePrerendersExceeded: |
| case PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl: |
| case PrerenderFinalStatus::kRedirectedPrerenderingUrlHasEffectiveUrl: |
| case PrerenderFinalStatus::kActivationUrlHasEffectiveUrl: |
| return PreloadingEligibility::kHasEffectiveUrl; |
| case PrerenderFinalStatus::kJavaScriptInterfaceAdded: |
| case PrerenderFinalStatus::kJavaScriptInterfaceRemoved: |
| case PrerenderFinalStatus::kAllPrerenderingCanceled: |
| case PrerenderFinalStatus::kWindowClosed: |
| case PrerenderFinalStatus::kOtherPrerenderedPageActivated: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kSlowNetwork: |
| return PreloadingEligibility::kSlowNetwork; |
| case PrerenderFinalStatus::kPrerenderFailedDuringPrefetch: |
| case PrerenderFinalStatus::kBrowsingDataRemoved: |
| NOTREACHED(); |
| case PrerenderFinalStatus::kPrerenderHostReused: |
| NOTREACHED(); |
| } |
| |
| NOTREACHED(); |
| } |
| |
| template <class ContainerType> |
| void DestructPrerenderHosts(ContainerType& hosts) { |
| // Swap the container and let it scope out instead of directly destructing the |
| // hosts in the container, for example, by `hosts.clear()`. This avoids |
| // potential cases where a host being deleted indirectly modifies the |
| // container while the container is being cleared up. |
| // See https://crbug.com/40263658 for contexts. |
| ContainerType temp; |
| hosts.swap(temp); |
| } |
| |
| // Represents a contract and ensures that the given prerender attempt is started |
| // as a PrerenderHost or rejected with a reason. It is allowed to use it only in |
| // PrerenderHostRegistry::CreateAndStartHost. |
| // |
| // TODO(kenoss): Add emits of Preload.prerenderStatusUpdated. |
| class PrerenderHostBuilder { |
| public: |
| explicit PrerenderHostBuilder(PreloadingAttempt* attempt); |
| ~PrerenderHostBuilder(); |
| |
| PrerenderHostBuilder(const PrerenderHostBuilder&) = delete; |
| PrerenderHostBuilder& operator=(const PrerenderHostBuilder&) = delete; |
| PrerenderHostBuilder(PrerenderHostBuilder&&) = delete; |
| PrerenderHostBuilder& operator=(PrerenderHostBuilder&&) = delete; |
| |
| // The following methods consumes this class. |
| std::unique_ptr<PrerenderHost> Build( |
| std::unique_ptr<PrerenderHost> reuse_host, |
| const PrerenderAttributes& attributes, |
| WebContentsImpl& prerender_web_contents); |
| void RejectAsNotEligible(const PrerenderAttributes& attributes, |
| PrerenderFinalStatus status); |
| void RejectAsDuplicate(); |
| void RejectAsFailure(const PrerenderAttributes& attributes, |
| PrerenderFinalStatus status); |
| void RejectDueToHoldback(); |
| |
| void SetHoldbackOverride(PreloadingHoldbackStatus status); |
| bool CheckIfShouldHoldback(); |
| |
| // Public only for exceptional case. |
| // TODO(crbug.com/40904828): Make this private again. |
| void Drop(); |
| |
| private: |
| bool IsDropped() const; |
| |
| // Use raw pointer as PrerenderHostBuilder is alive only during |
| // PrerenderHostRegistry::CreateAndStartHost(), and PreloadingAttempt should |
| // outlive the function. |
| raw_ptr<PreloadingAttempt> attempt_; |
| std::unique_ptr<DevToolsPrerenderAttempt> devtools_attempt_; |
| }; |
| |
| PrerenderHostBuilder::PrerenderHostBuilder(PreloadingAttempt* attempt) |
| : attempt_(attempt), |
| devtools_attempt_(std::make_unique<DevToolsPrerenderAttempt>()) {} |
| |
| PrerenderHostBuilder::~PrerenderHostBuilder() { |
| CHECK(IsDropped()); |
| } |
| |
| void PrerenderHostBuilder::Drop() { |
| attempt_ = nullptr; |
| devtools_attempt_.reset(); |
| } |
| |
| bool PrerenderHostBuilder::IsDropped() const { |
| return devtools_attempt_ == nullptr; |
| } |
| |
| std::unique_ptr<PrerenderHost> PrerenderHostBuilder::Build( |
| std::unique_ptr<PrerenderHost> reuse_host, |
| const PrerenderAttributes& attributes, |
| WebContentsImpl& prerender_web_contents) { |
| CHECK(!IsDropped()); |
| std::unique_ptr<PrerenderHost> prerender_host; |
| |
| prerender_host = std::make_unique<PrerenderHost>( |
| std::move(reuse_host), attributes, prerender_web_contents, |
| attempt_ ? attempt_->GetWeakPtr() : nullptr, |
| std::move(devtools_attempt_)); |
| Drop(); |
| |
| return prerender_host; |
| } |
| |
| void PrerenderHostBuilder::RejectAsNotEligible( |
| const PrerenderAttributes& attributes, |
| PrerenderFinalStatus status) { |
| CHECK(!IsDropped()); |
| |
| if (attempt_) { |
| attempt_->SetEligibility(ToEligibility(status)); |
| } |
| |
| devtools_attempt_->SetFailureReason(attributes, status); |
| |
| RecordFailedPrerenderFinalStatus(PrerenderCancellationReason(status), |
| attributes); |
| |
| Drop(); |
| } |
| |
| bool PrerenderHostBuilder::CheckIfShouldHoldback() { |
| CHECK(!IsDropped()); |
| |
| // Assigns the holdback status in the attempt it was not overridden earlier. |
| return attempt_ && attempt_->ShouldHoldback(); |
| } |
| |
| void PrerenderHostBuilder::RejectDueToHoldback() { |
| CHECK(!IsDropped()); |
| |
| // If DevTools is opened, holdbacks are force-disabled. So, we don't need to |
| // report this case to DevTools. |
| |
| Drop(); |
| } |
| |
| void PrerenderHostBuilder::RejectAsDuplicate() { |
| CHECK(!IsDropped()); |
| |
| if (attempt_) { |
| attempt_->SetTriggeringOutcome(PreloadingTriggeringOutcome::kDuplicate); |
| } |
| |
| // No need to report DevTools nor UMA; just removing duplicates. |
| |
| Drop(); |
| } |
| |
| void PrerenderHostBuilder::SetHoldbackOverride( |
| PreloadingHoldbackStatus status) { |
| if (!attempt_) { |
| return; |
| } |
| attempt_->SetHoldbackStatus(status); |
| } |
| |
| void PrerenderHostBuilder::RejectAsFailure( |
| const PrerenderAttributes& attributes, |
| PrerenderFinalStatus status) { |
| CHECK(!IsDropped()); |
| |
| if (attempt_) { |
| attempt_->SetFailureReason(ToPreloadingFailureReason(status)); |
| } |
| |
| devtools_attempt_->SetFailureReason(attributes, status); |
| |
| RecordFailedPrerenderFinalStatus(PrerenderCancellationReason(status), |
| attributes); |
| |
| Drop(); |
| } |
| |
| bool IsSlowNetwork(WebContents* web_contents) { |
| static const base::TimeDelta kSlowNetworkThreshold = |
| features::kSuppressesPrerenderingOnSlowNetworkThreshold.Get(); |
| return web_contents && web_contents->GetBrowserContext() && |
| web_contents->GetBrowserContext() |
| ->GetClientHintsControllerDelegate() && |
| web_contents->GetBrowserContext() |
| ->GetClientHintsControllerDelegate() |
| ->GetNetworkQualityTracker() && |
| web_contents->GetBrowserContext() |
| ->GetClientHintsControllerDelegate() |
| ->GetNetworkQualityTracker() |
| ->GetHttpRTT() > kSlowNetworkThreshold; |
| } |
| |
| } // namespace |
| |
| PrerenderHostRegistry::PrerenderHostRegistry(WebContents& web_contents) |
| : memory_pressure_listener_( |
| FROM_HERE, |
| base::MemoryPressureListenerTag::kPrerenderHostRegistry, |
| base::BindRepeating(&PrerenderHostRegistry::OnMemoryPressure, |
| base::Unretained(this))) { |
| Observe(&web_contents); |
| } |
| |
| PrerenderHostRegistry::~PrerenderHostRegistry() { |
| // This function is called by WebContentsImpl's dtor, so web_contents() should |
| // not be a null ptr at this moment. |
| CHECK(web_contents()); |
| |
| PrerenderFinalStatus final_status = |
| web_contents()->GetClosedByUserGesture() |
| ? PrerenderFinalStatus::kTabClosedByUserGesture |
| : PrerenderFinalStatus::kTabClosedWithoutUserGesture; |
| |
| // Here we have to delete the prerender hosts synchronously, to ensure the |
| // FrameTrees would not access the WebContents. |
| CancelAllHosts(final_status); |
| DestructPrerenderHosts(to_be_deleted_hosts_); |
| DestructPrerenderHosts(pending_deletion_hosts_); |
| |
| Observe(nullptr); |
| for (Observer& obs : observers_) |
| obs.OnRegistryDestroyed(); |
| } |
| |
| void PrerenderHostRegistry::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void PrerenderHostRegistry::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| FrameTreeNodeId PrerenderHostRegistry::CreateAndStartHost( |
| const PrerenderAttributes& attributes, |
| PreloadingAttempt* attempt) { |
| std::string recorded_url = |
| attributes.initiator_origin.has_value() |
| ? attributes.initiator_origin.value().GetURL().spec() |
| : "(empty_url)"; |
| |
| TRACE_EVENT2("navigation", "PrerenderHostRegistry::CreateAndStartHost", |
| "attributes", attributes, "initiator_origin", recorded_url); |
| |
| // The initiator WebContents can be different from the WebContents that will |
| // host a prerendered page only when the prerender-in-new-tab runs. |
| CHECK(attributes.initiator_web_contents); |
| auto& initiator_web_contents = |
| static_cast<WebContentsImpl&>(*attributes.initiator_web_contents); |
| auto& prerender_web_contents = static_cast<WebContentsImpl&>(*web_contents()); |
| |
| FrameTreeNodeId frame_tree_node_id; |
| |
| std::optional<blink::mojom::SpeculationEagerness> eagerness = |
| attributes.GetEagerness(); |
| |
| { |
| RenderFrameHostImpl* initiator_rfh = |
| attributes.IsBrowserInitiated() |
| ? nullptr |
| : RenderFrameHostImpl::FromFrameToken( |
| attributes.initiator_process_id, |
| attributes.initiator_frame_token.value()); |
| |
| // Ensure observers are notified that a trigger occurred. |
| absl::Cleanup notify_trigger = [this, &attributes] { |
| NotifyTrigger(attributes.prerendering_url); |
| }; |
| |
| auto builder = PrerenderHostBuilder(attempt); |
| |
| // We don't know the root cause, but there is a case this is null. |
| // |
| // TODO(crbug.com/40904828): Continue investigation and fix the root |
| // cause. |
| if (initiator_web_contents.GetDelegate() == nullptr) { |
| // Note that return without consuming `builder` is exceptional. |
| builder.Drop(); |
| return FrameTreeNodeId(); |
| } |
| |
| // Check the about://flags toggle. |
| if (!base::FeatureList::IsEnabled(blink::features::kPrerender2)) { |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kPreloadingDisabled); |
| return FrameTreeNodeId(); |
| } |
| |
| // Check whether preloading is enabled. If it is not enabled, report the |
| // reason. |
| switch (initiator_web_contents.GetDelegate()->IsPrerender2Supported( |
| initiator_web_contents, attributes.trigger_type)) { |
| case PreloadingEligibility::kEligible: |
| // nop |
| break; |
| case PreloadingEligibility::kPreloadingDisabled: |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kPreloadingDisabled); |
| return FrameTreeNodeId(); |
| case PreloadingEligibility::kDataSaverEnabled: |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kDataSaverEnabled); |
| return FrameTreeNodeId(); |
| case PreloadingEligibility::kBatterySaverEnabled: |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kBatterySaverEnabled); |
| return FrameTreeNodeId(); |
| case PreloadingEligibility::kPreloadingUnsupportedByWebContents: |
| builder.RejectAsNotEligible( |
| attributes, |
| PrerenderFinalStatus::kPreloadingUnsupportedByWebContents); |
| return FrameTreeNodeId(); |
| default: |
| NOTREACHED(); |
| } |
| |
| // Don't prerender when the initiator is in the background and its type is |
| // `kEmbedder`, as current implementation doesn't use `pending_prerenders_` |
| // when kEmbedder. |
| // If the trigger type is speculation rules, nothing should be done here and |
| // then prerender host will be created and its id will be enqueued to |
| // `pending_prerenders_`. The visibility of the initiator will be considered |
| // when trying to pop from `pending_prerenders_` on `StartPrerendering()`. |
| if (attributes.trigger_type == PreloadingTriggerType::kEmbedder && |
| initiator_web_contents.GetVisibility() == Visibility::HIDDEN) { |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kTriggerBackgrounded); |
| return FrameTreeNodeId(); |
| } |
| |
| // Don't prerender on low-end devices. |
| if (!DeviceHasEnoughMemoryForPrerender()) { |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kLowEndDevice); |
| return FrameTreeNodeId(); |
| } |
| |
| // Don't prerender under critical memory pressure. |
| switch (GetCurrentMemoryPressureLevel()) { |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE: |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE: |
| break; |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL: |
| builder.RejectAsNotEligible( |
| attributes, PrerenderFinalStatus::kMemoryPressureOnTrigger); |
| return FrameTreeNodeId(); |
| } |
| |
| // Disable prerendering on slow network. |
| static const bool kSuppressesPrerenderingOnSlowNetworkIsEnabled = |
| base::FeatureList::IsEnabled( |
| features::kSuppressesPrerenderingOnSlowNetwork); |
| if (kSuppressesPrerenderingOnSlowNetworkIsEnabled && |
| IsSlowNetwork(web_contents())) { |
| builder.RejectAsNotEligible(attributes, |
| PrerenderFinalStatus::kSlowNetwork); |
| return FrameTreeNodeId(); |
| } |
| |
| // Allow prerendering only for same-site. The initiator origin is nullopt |
| // when prerendering is initiated by the browser (not by a renderer using |
| // Speculation Rules API). In that case, skip this same-site check. |
| // TODO(crbug.com/40168192): Support cross-site prerendering. |
| if (!attributes.IsBrowserInitiated() && |
| !prerender_navigation_utils::IsSameSite( |
| attributes.prerendering_url, attributes.initiator_origin.value())) { |
| builder.RejectAsNotEligible( |
| attributes, |
| PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation); |
| return FrameTreeNodeId(); |
| } |
| |
| // Allow prerendering only HTTP(S) scheme URLs. For redirection, this will |
| // be checked in PrerenderNavigationThrottle::WillStartOrRedirectRequest(). |
| if (!attributes.prerendering_url.SchemeIsHTTPOrHTTPS()) { |
| builder.RejectAsNotEligible( |
| attributes, PrerenderFinalStatus::kInvalidSchemeNavigation); |
| return FrameTreeNodeId(); |
| } |
| |
| // Disallow all pages that have an effective URL like hosted apps and NTP. |
| auto* browser_context = prerender_web_contents.GetBrowserContext(); |
| if (SiteInstanceImpl::HasEffectiveURL(browser_context, |
| initiator_web_contents.GetURL())) { |
| builder.RejectAsNotEligible( |
| attributes, PrerenderFinalStatus::kTriggerUrlHasEffectiveUrl); |
| return FrameTreeNodeId(); |
| } |
| if (SiteInstanceImpl::HasEffectiveURL(browser_context, |
| attributes.prerendering_url)) { |
| builder.RejectAsNotEligible( |
| attributes, PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl); |
| return FrameTreeNodeId(); |
| } |
| |
| if (initiator_rfh && initiator_rfh->frame_tree() && |
| !devtools_instrumentation::IsPrerenderAllowed( |
| *initiator_rfh->frame_tree())) { |
| builder.RejectAsNotEligible( |
| attributes, PrerenderFinalStatus::kPrerenderingDisabledByDevTools); |
| return FrameTreeNodeId(); |
| } |
| |
| // Once all eligibility checks are completed, set the status to kEligible. |
| if (attempt) |
| attempt->SetEligibility(PreloadingEligibility::kEligible); |
| |
| // Normally CheckIfShouldHoldback() computes the holdback status based on |
| // PreloadingConfig. In special cases, we call SetHoldbackOverride() to |
| // override that processing. |
| bool has_devtools_open = |
| initiator_rfh && |
| RenderFrameDevToolsAgentHost::GetFor(initiator_rfh) != nullptr; |
| |
| if (has_devtools_open) { |
| // Never holdback when DevTools is opened, to avoid web developer |
| // frustration. |
| builder.SetHoldbackOverride(PreloadingHoldbackStatus::kAllowed); |
| } else if (attributes.holdback_status_override != |
| PreloadingHoldbackStatus::kUnspecified) { |
| // The caller (e.g. from chrome/) is allowed to specify a holdback that |
| // overrides the default logic. |
| builder.SetHoldbackOverride(attributes.holdback_status_override); |
| } |
| |
| // Check if the attempt is held back either due to the check above or via |
| // PreloadingConfig. |
| if (builder.CheckIfShouldHoldback()) { |
| builder.RejectDueToHoldback(); |
| return FrameTreeNodeId(); |
| } |
| |
| // Ignore prerendering requests for the same URL. |
| for (auto& iter : prerender_host_by_frame_tree_node_id_) { |
| if (iter.second->GetInitialUrl() == attributes.prerendering_url) { |
| builder.RejectAsDuplicate(); |
| return FrameTreeNodeId(); |
| } |
| } |
| |
| // CreateAndStartHost can be called in the newly created WebContents's |
| // PrerenderHostRegistry for new tab triggers, rather than in initiator |
| // WebContents's registry, while it is called in initiator ones for normal |
| // triggers. In either case, we want to control the limit based on the |
| // initiator WebContents. |
| // |
| // TODO(crbug.com/40235847): Enqueue the request exceeding the number limit |
| // until the forerunners are cancelled, and suspend starting a new prerender |
| // when the number reaches the limit. |
| if (!initiator_web_contents.GetPrerenderHostRegistry() |
| ->IsAllowedToStartPrerenderingForTrigger(attributes.trigger_type, |
| eagerness)) { |
| // The reason we don't consider limit exceeded as an ineligibility |
| // reason is because we can't replicate the behavior in our other |
| // experiment groups for analysis. To prevent this we set |
| // TriggeringOutcome to kFailure and look into the failure reason to |
| // learn more. |
| PrerenderFinalStatus final_status; |
| switch (GetPrerenderLimitGroup(attributes.trigger_type, eagerness)) { |
| case PrerenderLimitGroup::kSpeculationRulesImmediate: |
| final_status = |
| PrerenderFinalStatus::kMaxNumOfRunningImmediatePrerendersExceeded; |
| break; |
| case PrerenderLimitGroup::kSpeculationRulesNonImmediate: |
| final_status = PrerenderFinalStatus:: |
| kMaxNumOfRunningNonImmediatePrerendersExceeded; |
| break; |
| case PrerenderLimitGroup::kEmbedder: |
| final_status = |
| PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded; |
| break; |
| } |
| builder.RejectAsFailure(attributes, final_status); |
| return FrameTreeNodeId(); |
| } |
| |
| std::unique_ptr<PrerenderHost> reuse_host; |
| std::unique_ptr<PrerenderHost> prerender_host; |
| if (base::FeatureList::IsEnabled(features::kPrerender2ReuseHost)) { |
| reuse_host = FindAndTakePrerenderHostToReuse(attributes); |
| } |
| base::UmaHistogramBoolean("Prerender.Experimental.FoundReusePrerenderHost", |
| reuse_host != nullptr); |
| if (!reuse_host) { |
| base::UmaHistogramCounts100( |
| "Prerender.Experimental.ReusePrerenderHost.PrerenderHostCount.Failed", |
| prerender_host_by_frame_tree_node_id_.size()); |
| } |
| // If we find a reusable prerender host under the same site. We will |
| // take over its frame tree and initiate a new navigation to the new |
| // prerender URL. |
| prerender_host = builder.Build(std::move(reuse_host), attributes, |
| prerender_web_contents); |
| frame_tree_node_id = prerender_host->frame_tree_node_id(); |
| |
| CHECK(!base::Contains(prerender_host_by_frame_tree_node_id_, |
| frame_tree_node_id)); |
| prerender_host_by_frame_tree_node_id_[frame_tree_node_id] = |
| std::move(prerender_host); |
| |
| if (GetPrerenderLimitGroup(attributes.trigger_type, eagerness) == |
| PrerenderLimitGroup::kSpeculationRulesNonImmediate) { |
| non_immediate_prerender_host_id_by_arrival_order_.push_back( |
| frame_tree_node_id); |
| } |
| } |
| |
| // Now start prerender the new page. If the PrerenderHost is reusing a frame |
| // tree, the previous page will be unloaded after initiating a new navigation. |
| switch (attributes.trigger_type) { |
| case PreloadingTriggerType::kSpeculationRule: |
| case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld: |
| case PreloadingTriggerType::kSpeculationRuleFromAutoSpeculationRules: |
| pending_prerenders_.push_back(frame_tree_node_id); |
| if (running_prerender_host_id_.is_null()) { |
| // Start the initial prerendering navigation of the pending request in |
| // the head of the queue if there's no running prerender and the |
| // initiator is in the foreground. If the initiator page is in the |
| // background, `StartPrerendering` will return a corresponding |
| // frame_tree_node_id if allowed by |
| // `PrerenderCanBeStartedWhenInitiatorIsInBackground`. |
| if (IsBackground(initiator_web_contents.GetVisibility()) && |
| !initiator_web_contents.GetPrerenderHostRegistry() |
| ->PrerenderCanBeStartedWhenInitiatorIsInBackground()) { |
| // Cancel if it is prerender-into-new-tab. |
| // TODO(crbug.com/350785853): Add queue |
| // mechanism and update test expectation. |
| if (web_contents() != &initiator_web_contents) { |
| return FrameTreeNodeId(); |
| } |
| break; |
| } |
| FrameTreeNodeId started_frame_tree_node_id = |
| StartPrerendering(FrameTreeNodeId()); |
| CHECK(started_frame_tree_node_id == frame_tree_node_id || |
| started_frame_tree_node_id.is_null()); |
| frame_tree_node_id = started_frame_tree_node_id; |
| } |
| break; |
| case PreloadingTriggerType::kEmbedder: |
| // The prerendering request from embedder should have high-priority |
| // because embedder prediction is more likely for the user to visit. Hold |
| // the return value of `StartPrerendering` because the requested prerender |
| // might be cancelled due to some restrictions and a null FrameTreeNodeId |
| // should be returned in that case. |
| frame_tree_node_id = StartPrerendering(frame_tree_node_id); |
| break; |
| } |
| |
| return frame_tree_node_id; |
| } |
| |
| FrameTreeNodeId PrerenderHostRegistry::CreateAndStartHostForNewTab( |
| const PrerenderAttributes& attributes, |
| const PreloadingPredictor& creating_predictor, |
| const PreloadingPredictor& enacting_predictor, |
| PreloadingConfidence confidence) { |
| CHECK(IsSpeculationRuleType(attributes.trigger_type)); |
| std::string recorded_url = |
| attributes.initiator_origin.has_value() |
| ? attributes.initiator_origin.value().GetURL().spec() |
| : "(empty_url)"; |
| TRACE_EVENT2("navigation", |
| "PrerenderHostRegistry::CreateAndStartHostForNewTab", |
| "attributes", attributes, "initiator_origin", recorded_url); |
| |
| auto handle = std::make_unique<PrerenderNewTabHandle>( |
| attributes, *web_contents()->GetBrowserContext()); |
| FrameTreeNodeId prerender_host_id = handle->StartPrerendering( |
| creating_predictor, enacting_predictor, confidence); |
| if (prerender_host_id.is_null()) { |
| return FrameTreeNodeId(); |
| } |
| prerender_new_tab_handle_by_frame_tree_node_id_[prerender_host_id] = |
| std::move(handle); |
| |
| if (GetPrerenderLimitGroup(attributes.trigger_type, |
| attributes.GetEagerness()) == |
| PrerenderLimitGroup::kSpeculationRulesNonImmediate) { |
| non_immediate_prerender_host_id_by_arrival_order_.push_back( |
| prerender_host_id); |
| } |
| return prerender_host_id; |
| } |
| |
| FrameTreeNodeId PrerenderHostRegistry::StartPrerendering( |
| FrameTreeNodeId frame_tree_node_id) { |
| // TODO(crbug.com/40260412): Don't start prerendering if the current |
| // memory pressure level is critical, and then retry prerendering when the |
| // memory pressure level goes down. |
| |
| if (frame_tree_node_id.is_null()) { |
| CHECK(running_prerender_host_id_.is_null()); |
| |
| while (!pending_prerenders_.empty()) { |
| FrameTreeNodeId host_id = pending_prerenders_.front(); |
| |
| // Skip a cancelled request. |
| auto found = prerender_host_by_frame_tree_node_id_.find(host_id); |
| if (found == prerender_host_by_frame_tree_node_id_.end()) { |
| // Remove the cancelled request from the pending queue. |
| pending_prerenders_.pop_front(); |
| continue; |
| } |
| PrerenderHost* prerender_host = found->second.get(); |
| |
| // The initiator WebContents should be alive as it cancels all the |
| // prerendering requests during destruction. |
| CHECK(prerender_host->initiator_web_contents()); |
| |
| WebContentsImpl* initiator_web_contents = static_cast<WebContentsImpl*>( |
| prerender_host->initiator_web_contents().get()); |
| if (IsBackground(initiator_web_contents->GetVisibility())) { |
| // The pending prerender triggered by the background tab will be started |
| // according to the conditions in |
| // `PrerenderCanBeStartedWhenInitiatorIsInBackground`. |
| if (!initiator_web_contents->GetPrerenderHostRegistry() |
| ->PrerenderCanBeStartedWhenInitiatorIsInBackground()) { |
| return FrameTreeNodeId(); |
| } |
| } |
| |
| // Found the request to run. |
| pending_prerenders_.pop_front(); |
| frame_tree_node_id = host_id; |
| break; |
| } |
| |
| if (frame_tree_node_id.is_null()) { |
| return FrameTreeNodeId(); |
| } |
| } |
| |
| auto prerender_host_it = |
| prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id); |
| CHECK(prerender_host_it != prerender_host_by_frame_tree_node_id_.end()); |
| PrerenderHost& prerender_host = *prerender_host_it->second; |
| devtools_instrumentation::WillInitiatePrerender( |
| prerender_host.GetPrerenderFrameTree()); |
| if (!prerender_host.StartPrerendering()) { |
| CancelHost(frame_tree_node_id, PrerenderFinalStatus::kStartFailed); |
| return FrameTreeNodeId(); |
| } |
| |
| switch (prerender_host_by_frame_tree_node_id_[frame_tree_node_id] |
| ->trigger_type()) { |
| case PreloadingTriggerType::kSpeculationRule: |
| case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld: |
| case PreloadingTriggerType::kSpeculationRuleFromAutoSpeculationRules: |
| // Update the `running_prerender_host_id` to the starting prerender's id. |
| running_prerender_host_id_ = frame_tree_node_id; |
| break; |
| case PreloadingTriggerType::kEmbedder: |
| // `running_prerender_host_id` only tracks the id for speculation rules |
| // trigger, so we also don't update it in the case of embedder. |
| break; |
| } |
| |
| RecordPrerenderTriggered( |
| prerender_host_by_frame_tree_node_id_[frame_tree_node_id] |
| ->initiator_ukm_id()); |
| return frame_tree_node_id; |
| } |
| |
| std::set<FrameTreeNodeId> PrerenderHostRegistry::CancelHosts( |
| const std::vector<FrameTreeNodeId>& frame_tree_node_ids, |
| const PrerenderCancellationReason& reason) { |
| TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHosts", |
| "frame_tree_node_ids", frame_tree_node_ids); |
| |
| // Cancel must not be requested during activation. |
| CHECK(!reserved_prerender_host_); |
| |
| std::set<FrameTreeNodeId> cancelled_ids; |
| |
| for (FrameTreeNodeId host_id : frame_tree_node_ids) { |
| if (CancelHostInternal(host_id, reason) || |
| CancelNewTabHostInternal(host_id, reason)) { |
| cancelled_ids.insert(host_id); |
| } |
| } |
| |
| // Start another prerender if the running prerender is cancelled. |
| if (running_prerender_host_id_.is_null()) { |
| StartPrerendering(FrameTreeNodeId()); |
| } |
| |
| return cancelled_ids; |
| } |
| |
| bool PrerenderHostRegistry::CancelHost(FrameTreeNodeId frame_tree_node_id, |
| PrerenderFinalStatus final_status) { |
| return CancelHost(frame_tree_node_id, |
| PrerenderCancellationReason(final_status)); |
| } |
| |
| bool PrerenderHostRegistry::CancelHost( |
| FrameTreeNodeId frame_tree_node_id, |
| const PrerenderCancellationReason& reason) { |
| TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHost", |
| "frame_tree_node_id", frame_tree_node_id); |
| std::set<FrameTreeNodeId> cancelled_ids = |
| CancelHosts({frame_tree_node_id}, reason); |
| return !cancelled_ids.empty(); |
| } |
| |
| void PrerenderHostRegistry::CancelHostsForTriggers( |
| std::vector<PreloadingTriggerType> trigger_types, |
| const PrerenderCancellationReason& reason) { |
| TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHostsForTrigger", |
| "trigger_type", trigger_types[0]); |
| |
| std::vector<FrameTreeNodeId> ids_to_be_deleted; |
| |
| for (auto& iter : prerender_host_by_frame_tree_node_id_) { |
| if (base::Contains(trigger_types, iter.second->trigger_type())) { |
| ids_to_be_deleted.push_back(iter.first); |
| } |
| } |
| for (auto& iter : prerender_new_tab_handle_by_frame_tree_node_id_) { |
| if (base::Contains(trigger_types, iter.second->trigger_type())) { |
| // Prerendering into a new tab can be triggered by speculation rules only. |
| CHECK(IsSpeculationRuleType(iter.second->trigger_type())); |
| ids_to_be_deleted.push_back(iter.first); |
| } |
| } |
| |
| CancelHosts(ids_to_be_deleted, reason); |
| } |
| |
| void PrerenderHostRegistry::CancelAllHosts(PrerenderFinalStatus final_status) { |
| // Cancel must not be requested during activation. |
| CHECK(!reserved_prerender_host_); |
| |
| PrerenderCancellationReason reason(final_status); |
| |
| while (!prerender_host_by_frame_tree_node_id_.empty()) { |
| CancelHostInternal(prerender_host_by_frame_tree_node_id_.begin()->first, |
| reason); |
| } |
| |
| while (!prerender_new_tab_handle_by_frame_tree_node_id_.empty()) { |
| CancelNewTabHostInternal( |
| prerender_new_tab_handle_by_frame_tree_node_id_.begin()->first, reason); |
| } |
| |
| pending_prerenders_.clear(); |
| } |
| |
| bool PrerenderHostRegistry::CancelHostInternal( |
| FrameTreeNodeId frame_tree_node_id, |
| const PrerenderCancellationReason& reason) { |
| // Look up the id in the non-reserved host map. |
| auto iter = prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id); |
| if (iter == prerender_host_by_frame_tree_node_id_.end()) { |
| return false; |
| } |
| |
| if (running_prerender_host_id_ == frame_tree_node_id) { |
| running_prerender_host_id_ = FrameTreeNodeId(); |
| } |
| |
| // Remove the prerender host from the host map so that it's not used for |
| // activation during asynchronous deletion. |
| std::unique_ptr<PrerenderHost> prerender_host = std::move(iter->second); |
| prerender_host_by_frame_tree_node_id_.erase(iter); |
| |
| prerender_host->OnWillBeCancelled(reason); |
| reason.ReportMetrics(prerender_host->GetHistogramSuffix()); |
| |
| NotifyCancel(prerender_host->frame_tree_node_id(), reason); |
| |
| // If the host we are attempting to cancel is the new-tab host and initiator |
| // WebContents's PrerenderHostRegistry for this host is still alive, invoke |
| // the initiator WebContents's CancelNewTabHostInternal to destroy |
| // PrerenderNewTabHandle and WebContents that this new-tab host belongs to. |
| // This will eventually destroy `this`, so it should be performed |
| // asynchronously. |
| WebContentsImpl* initiator_web_contents = static_cast<WebContentsImpl*>( |
| prerender_host->initiator_web_contents().get()); |
| // The initiator WebContents may not be alive. |
| // See crrev.com/c/6286546/comment/1adfe28c_4f769aa7 for more details. |
| if (initiator_web_contents && web_contents() != initiator_web_contents && |
| !initiator_web_contents->IsBeingDestroyed()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| base::IgnoreResult( |
| &PrerenderHostRegistry::CancelNewTabHostInternal), |
| initiator_web_contents->GetPrerenderHostRegistry()->GetWeakPtr(), |
| frame_tree_node_id, |
| PrerenderCancellationReason(reason.final_status()))); |
| } |
| |
| // Asynchronously delete the prerender host. |
| ScheduleToDeleteAbandonedHost(std::move(prerender_host), reason); |
| return true; |
| } |
| |
| bool PrerenderHostRegistry::CancelNewTabHostInternal( |
| FrameTreeNodeId frame_tree_node_id, |
| const PrerenderCancellationReason& reason) { |
| // Look up the id in the prerender-in-new-tab handle map. |
| auto iter = |
| prerender_new_tab_handle_by_frame_tree_node_id_.find(frame_tree_node_id); |
| if (iter == prerender_new_tab_handle_by_frame_tree_node_id_.end()) { |
| return false; |
| } |
| |
| // The host should be driven by PrerenderHostRegistry associated with |
| // the new tab. |
| CHECK_NE(running_prerender_host_id_, frame_tree_node_id); |
| |
| std::unique_ptr<PrerenderNewTabHandle> handle = std::move(iter->second); |
| prerender_new_tab_handle_by_frame_tree_node_id_.erase(iter); |
| NotifyCancel(frame_tree_node_id, reason); |
| |
| if (reason.final_status() == PrerenderFinalStatus::kSpeculationRuleRemoved) { |
| auto& new_tab_registry = handle->GetPrerenderHostRegistry(); |
| new_tab_registry.SchedulePendingDeletionPrerenderNewTabHandle( |
| std::move(handle)); |
| new_tab_registry.CancelHost(frame_tree_node_id, reason); |
| } else { |
| handle->CancelPrerendering(reason); |
| } |
| |
| return true; |
| } |
| |
| FrameTreeNodeId PrerenderHostRegistry::FindPotentialHostToActivate( |
| NavigationRequest& navigation_request) { |
| TRACE_EVENT2( |
| "navigation", "PrerenderHostRegistry::FindPotentialHostToActivate", |
| "navigation_url", navigation_request.GetURL().spec(), "render_frame_host", |
| navigation_request.frame_tree_node()->current_frame_host()); |
| |
| // Disallow activation when the navigation is for a nested browsing context |
| // (e.g., iframes, fenced frames). This is because nested browsing contexts |
| // such as iframes are supposed to be created in the parent's browsing context |
| // group and can script with the parent, but prerendered pages are created in |
| // new browsing context groups. |
| // |
| // Also, disallow activation when the navigation happens in the prerendering |
| // frame tree. |
| if (!navigation_request.IsInPrimaryMainFrame()) { |
| return FrameTreeNodeId(); |
| } |
| |
| // Collect hosts that can match the navigation request. |
| std::vector<PrerenderHost*> matchable_hosts; |
| // First, collect hosts that can exactly match or match with the |
| // No-Vary-Search header. |
| for (const auto& [host_id, host] : prerender_host_by_frame_tree_node_id_) { |
| if (host->IsUrlMatch(navigation_request.GetURL())) { |
| matchable_hosts.push_back(host.get()); |
| } |
| } |
| // Then, collect hosts that can match with the No-Vary-Search hint. |
| for (const auto& [host_id, host] : prerender_host_by_frame_tree_node_id_) { |
| if (host->IsNoVarySearchHintUrlMatch(navigation_request.GetURL())) { |
| matchable_hosts.push_back(host.get()); |
| } |
| } |
| RecordPotentialPrerenderProcessReuse(!matchable_hosts.empty(), |
| navigation_request.GetURL()); |
| if (matchable_hosts.empty()) { |
| return FrameTreeNodeId(); |
| } |
| // Use the first match. This prioritizes the exact match or No-Vary-Search |
| // header match than No-Vary-Search hint match. |
| PrerenderHost* host = *matchable_hosts.begin(); |
| |
| base::UmaHistogramCounts100( |
| "Prerender.Experimental.MatchableHostCountOnActivation", |
| matchable_hosts.size()); |
| // Cannot activate if prerendering navigation has not started yet. |
| if (!host->GetInitialNavigationId().has_value()) { |
| CancelHost(host->frame_tree_node_id(), |
| PrerenderFinalStatus::kActivatedBeforeStarted); |
| return FrameTreeNodeId(); |
| } |
| |
| return CanNavigationActivateHost(navigation_request, *host) |
| ? host->frame_tree_node_id() |
| : FrameTreeNodeId(); |
| } |
| |
| std::optional<ReservedPrerenderHostInfo> |
| PrerenderHostRegistry::ReserveHostToActivate( |
| NavigationRequest& navigation_request, |
| FrameTreeNodeId expected_host_id) { |
| RenderFrameHostImpl* render_frame_host = |
| navigation_request.frame_tree_node()->current_frame_host(); |
| TRACE_EVENT2("navigation", "PrerenderHostRegistry::ReserveHostToActivate", |
| "navigation_url", navigation_request.GetURL().spec(), |
| "render_frame_host", render_frame_host); |
| |
| CHECK(navigation_request.IsInPrimaryMainFrame()); |
| |
| // Choose the host that NavigationRequest expects. |
| // |
| // Note that other prerendered pages may match this NavigationRequest but |
| // we shouldn't activate them. NavigationRequest makes sure that the |
| // expected prerendered page is ready for activation by waiting for |
| // PrerenderCommitDeferringCondition before this point, while the new |
| // matched pages may not be ready for activation yet. |
| auto it = prerender_host_by_frame_tree_node_id_.find(expected_host_id); |
| if (it == prerender_host_by_frame_tree_node_id_.end()) { |
| return std::nullopt; |
| } |
| |
| PrerenderHost& host_ref = *it->second; |
| |
| // The expected host does not match. This can happen when the host matches |
| // based on the No-Vary-Search hint but actually it does not match based on |
| // the No-Vary-Search header. |
| std::optional<UrlMatchType> match_type = |
| host_ref.IsUrlMatch(navigation_request.GetURL()); |
| if (!match_type.has_value()) { |
| return std::nullopt; |
| } |
| |
| if (!CanNavigationActivateHost(navigation_request, host_ref)) { |
| return std::nullopt; |
| } |
| |
| FrameTreeNodeId host_id = host_ref.frame_tree_node_id(); |
| |
| // Disallow activation when ongoing navigations exist. It can happen when the |
| // main frame navigation starts after PrerenderCommitDeferringCondition posts |
| // a task to resume activation and before the activation is completed. |
| auto& prerender_frame_tree = host_ref.GetPrerenderFrameTree(); |
| if (prerender_frame_tree.root()->HasNavigation()) { |
| CancelHost(host_id, |
| PrerenderFinalStatus::kActivatedDuringMainFrameNavigation); |
| return std::nullopt; |
| } |
| |
| // Remove the host from the map of non-reserved hosts. |
| std::unique_ptr<PrerenderHost> host = |
| std::move(prerender_host_by_frame_tree_node_id_[host_id]); |
| prerender_host_by_frame_tree_node_id_.erase(host_id); |
| CHECK_EQ(host_id, host->frame_tree_node_id()); |
| CHECK(host->IsUrlMatch(navigation_request.GetURL())); |
| |
| if (match_type.value() == UrlMatchType::kNoVarySearch) { |
| // Count use of No-Vary-Search header in prerender. |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| web_contents()->GetPrimaryMainFrame(), |
| blink::mojom::WebFeature::kNoVarySearchPrerender); |
| } |
| // Reserve the host for activation. |
| CHECK(!reserved_prerender_host_); |
| reserved_prerender_host_ = std::move(host); |
| |
| return ReservedPrerenderHostInfo( |
| host_id, reserved_prerender_host_->trigger_type(), |
| reserved_prerender_host_->embedder_histogram_suffix(), |
| reserved_prerender_host_->host_reused()); |
| } |
| |
| RenderFrameHostImpl* PrerenderHostRegistry::GetRenderFrameHostForReservedHost( |
| FrameTreeNodeId frame_tree_node_id) { |
| if (!reserved_prerender_host_) |
| return nullptr; |
| |
| CHECK_EQ(frame_tree_node_id, reserved_prerender_host_->frame_tree_node_id()); |
| |
| return reserved_prerender_host_->GetPrerenderedMainFrameHost(); |
| } |
| |
| std::unique_ptr<StoredPage> PrerenderHostRegistry::ActivateReservedHost( |
| FrameTreeNodeId frame_tree_node_id, |
| NavigationRequest& navigation_request) { |
| CHECK(reserved_prerender_host_); |
| CHECK_EQ(frame_tree_node_id, reserved_prerender_host_->frame_tree_node_id()); |
| |
| std::unique_ptr<PrerenderHost> prerender_host = |
| std::move(reserved_prerender_host_); |
| return prerender_host->Activate(navigation_request); |
| } |
| |
| void PrerenderHostRegistry::OnActivationFinished( |
| FrameTreeNodeId frame_tree_node_id) { |
| // OnActivationFinished() should not be called for non-reserved hosts. |
| CHECK(!base::Contains(prerender_host_by_frame_tree_node_id_, |
| frame_tree_node_id)); |
| |
| if (!reserved_prerender_host_) { |
| // The activation finished successfully and has already activated the |
| // reserved host. |
| return; |
| } |
| |
| // The activation navigation is cancelled before activating the prerendered |
| // page, which means the activation failed. |
| CHECK_EQ(frame_tree_node_id, reserved_prerender_host_->frame_tree_node_id()); |
| |
| // TODO(crbug.com/40243805): Monitor the final status metric and see |
| // whether it could be possible. |
| ScheduleToDeleteAbandonedHost( |
| std::move(reserved_prerender_host_), |
| PrerenderCancellationReason( |
| PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess)); |
| } |
| |
| PrerenderHost* PrerenderHostRegistry::FindNonReservedHostById( |
| FrameTreeNodeId frame_tree_node_id) { |
| auto id_iter = prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id); |
| if (id_iter == prerender_host_by_frame_tree_node_id_.end()) |
| return nullptr; |
| return id_iter->second.get(); |
| } |
| |
| bool PrerenderHostRegistry::HasReservedHost() const { |
| return !!reserved_prerender_host_; |
| } |
| |
| std::unique_ptr<WebContentsImpl> |
| PrerenderHostRegistry::TakePreCreatedWebContentsForNewTabIfExists( |
| const mojom::CreateNewWindowParams& create_new_window_params, |
| const WebContents::CreateParams& web_contents_create_params) { |
| // Don't serve a prerendered page if the window needs the opener or is created |
| // for non-regular navigations. |
| if (!create_new_window_params.opener_suppressed || |
| create_new_window_params.is_form_submission || |
| create_new_window_params.pip_options) { |
| return nullptr; |
| } |
| |
| for (auto& iter : prerender_new_tab_handle_by_frame_tree_node_id_) { |
| std::unique_ptr<WebContentsImpl> web_contents = |
| iter.second->TakeWebContentsIfAvailable(create_new_window_params, |
| web_contents_create_params); |
| if (web_contents) { |
| prerender_new_tab_handle_by_frame_tree_node_id_.erase(iter); |
| return web_contents; |
| } |
| } |
| return nullptr; |
| } |
| |
| std::vector<FrameTree*> PrerenderHostRegistry::GetPrerenderFrameTrees() { |
| std::vector<FrameTree*> result; |
| for (auto& i : prerender_host_by_frame_tree_node_id_) { |
| result.push_back(&i.second->GetPrerenderFrameTree()); |
| } |
| if (reserved_prerender_host_) |
| result.push_back(&reserved_prerender_host_->GetPrerenderFrameTree()); |
| |
| return result; |
| } |
| |
| PrerenderHost* PrerenderHostRegistry::FindHostByUrlForTesting( |
| const GURL& prerendering_url) { |
| for (auto& iter : prerender_host_by_frame_tree_node_id_) { |
| if (iter.second->IsUrlMatch(prerendering_url)) { |
| return iter.second.get(); |
| } |
| } |
| return nullptr; |
| } |
| |
| PrerenderHost* PrerenderHostRegistry::FindPrewarmSearchResultHostForTesting( |
| const GURL& search_prewarm_url) { |
| for (auto& iter : prerender_host_by_frame_tree_node_id_) { |
| if (iter.second->GetInitialUrl() == search_prewarm_url) { |
| return iter.second.get(); |
| } |
| } |
| return nullptr; |
| } |
| |
| bool PrerenderHostRegistry::HasNewTabHandleByIdForTesting( |
| FrameTreeNodeId frame_tree_node_id) { |
| return prerender_new_tab_handle_by_frame_tree_node_id_.contains( |
| frame_tree_node_id); |
| } |
| |
| void PrerenderHostRegistry::CancelAllHostsForTesting() { |
| CHECK(!reserved_prerender_host_) |
| << "It is not possible to cancel a reserved host, so they must not exist " |
| "when trying to cancel all hosts"; |
| |
| for (auto& iter : prerender_host_by_frame_tree_node_id_) { |
| // Asynchronously delete the prerender host. |
| ScheduleToDeleteAbandonedHost( |
| std::move(iter.second), |
| PrerenderCancellationReason( |
| PrerenderFinalStatus::kCancelAllHostsForTesting)); |
| } |
| |
| // After we're done scheduling deletion, clear the map and the pending queue. |
| prerender_host_by_frame_tree_node_id_.clear(); |
| pending_prerenders_.clear(); |
| } |
| |
| void PrerenderHostRegistry::BackNavigationLikely( |
| PreloadingPredictor predictor) { |
| if (http_cache_query_loader_) { |
| return; |
| } |
| |
| PreloadingDataImpl* preloading_data = |
| PreloadingDataImpl::GetOrCreateForWebContents(web_contents()); |
| preloading_data->SetIsNavigationInDomainCallback( |
| predictor, |
| base::BindRepeating(IsNavigationInSessionHistoryPredictorDomain)); |
| |
| WebContentsImpl* contents = static_cast<WebContentsImpl*>(web_contents()); |
| NavigationControllerImpl& controller = contents->GetController(); |
| const std::optional<int> target_index = controller.GetIndexForGoBack(); |
| |
| if (!target_index.has_value()) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kNoBackEntry, nullptr); |
| return; |
| } |
| |
| NavigationEntryImpl* back_entry = controller.GetEntryAtIndex(*target_index); |
| CHECK(back_entry); |
| const GURL& back_url = back_entry->GetURL(); |
| |
| if (controller.GetBackForwardCache() |
| .GetOrEvictEntry(back_entry->GetUniqueID()) |
| .has_value()) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kBfcacheEntryExists, |
| nullptr); |
| return; |
| } |
| |
| PreloadingURLMatchCallback same_url_matcher = |
| PreloadingData::GetSameURLMatcher(back_url); |
| ukm::SourceId triggered_primary_page_source_id = |
| web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId(); |
| preloading_data->AddPreloadingPrediction(predictor, PreloadingConfidence{100}, |
| same_url_matcher, |
| triggered_primary_page_source_id); |
| PreloadingAttempt* attempt = preloading_data->AddPreloadingAttempt( |
| predictor, PreloadingType::kPrerender, same_url_matcher, |
| triggered_primary_page_source_id); |
| |
| if (back_entry->GetMainFrameDocumentSequenceNumber() == |
| controller.GetLastCommittedEntry() |
| ->GetMainFrameDocumentSequenceNumber()) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kTargetIsSameDocument, |
| attempt); |
| return; |
| } |
| |
| if (back_entry->root_node()->frame_entry->method() != "GET") { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kMethodNotGet, attempt); |
| return; |
| } |
| |
| if (prerender_navigation_utils::IsDisallowedHttpResponseCode( |
| back_entry->GetHttpStatusCode())) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, |
| PrerenderBackNavigationEligibility::kTargetIsFailedNavigation, attempt); |
| return; |
| } |
| |
| if (!back_url.SchemeIsHTTPOrHTTPS()) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kTargetIsNonHttp, |
| attempt); |
| return; |
| } |
| |
| // While same site back navigations could potentially be prerendered, doing so |
| // would involve more significant compat risk. For now, we consider them |
| // ineligible. See https://crbug.com/1422266 . |
| if (prerender_navigation_utils::IsSameSite( |
| back_url, |
| contents->GetPrimaryMainFrame()->GetLastCommittedOrigin())) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kTargetIsSameSite, |
| attempt); |
| return; |
| } |
| |
| // Session history prerendering will reuse the back navigation entry's |
| // existing SiteInstance. We can't have a prerendered document share a |
| // SiteInstance with related active content (i.e. an active document with the |
| // same BrowsingInstance) as that would risk having scripting connections to |
| // prerendered documents. So this case is not eligible for prerendering. |
| SiteInstanceImpl* entry_site_instance = back_entry->site_instance(); |
| // `entry_site_instance` could be null in cases such as session restore. |
| if (entry_site_instance) { |
| const bool current_and_target_related = |
| contents->GetSiteInstance()->IsRelatedSiteInstance(entry_site_instance); |
| const size_t allowable_related_count = current_and_target_related ? 1u : 0u; |
| if (entry_site_instance->GetRelatedActiveContentsCount() > |
| allowable_related_count) { |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kRelatedActiveContents, |
| attempt); |
| return; |
| } |
| } |
| |
| // To determine whether the resource for the target entry is in the HTTP |
| // cache, we send a "fake" ResourceRequest which only loads from the cache. |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory = |
| contents->GetPrimaryMainFrame() |
| ->GetStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| http_cache_query_loader_ = CreateHttpCacheQueryingResourceLoad(back_url); |
| http_cache_query_loader_->DownloadHeadersOnly( |
| url_loader_factory.get(), |
| base::BindOnce(&PrerenderHostRegistry::OnBackResourceCacheResult, |
| base::Unretained(this), predictor, attempt->GetWeakPtr(), |
| back_url)); |
| } |
| |
| void PrerenderHostRegistry::OnBackResourceCacheResult( |
| PreloadingPredictor predictor, |
| base::WeakPtr<PreloadingAttempt> attempt, |
| GURL back_url, |
| scoped_refptr<net::HttpResponseHeaders> headers) { |
| // It's safe to delete the SimpleURLLoader while running the callback that was |
| // passed to it. We do so once we're done with it in this method. |
| std::unique_ptr<network::SimpleURLLoader> http_cache_query_loader = |
| std::move(http_cache_query_loader_); |
| |
| if (!http_cache_query_loader->LoadedFromCache()) { |
| // If not in the cache, then this cache-only request must have failed. |
| CHECK_NE(http_cache_query_loader->NetError(), net::OK); |
| |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kNoHttpCacheEntry, |
| attempt.get()); |
| return; |
| } |
| |
| RecordPrerenderBackNavigationEligibility( |
| predictor, PrerenderBackNavigationEligibility::kEligible, attempt.get()); |
| |
| if (attempt) { |
| attempt->SetHoldbackStatus(PreloadingHoldbackStatus::kAllowed); |
| // At this point, we are only collecting metrics and not actually |
| // prerendering anything. |
| attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kNoOp); |
| } |
| } |
| |
| base::WeakPtr<PrerenderHostRegistry> PrerenderHostRegistry::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| PrerenderHostId PrerenderHostRegistry::GetPrerenderHostIdForNavigation( |
| NavigationRequest* navigation_request) { |
| PrerenderHost* prerender_host = nullptr; |
| if (navigation_request->IsInPrerenderedMainFrame()) { |
| // This navigation is running on the main frame in the prerendered page, so |
| // its FrameTree::Delegate should be PrerenderHost. |
| prerender_host = &PrerenderHost::GetFromFrameTreeNode( |
| *navigation_request->frame_tree_node()); |
| } else { |
| // Since the navigation in the fenced frames are deferred until the |
| // activation, we do not need to check the outermost main frame for |
| // navigation requests in prerendered pages. |
| FrameTreeNodeId main_frame_host_id = navigation_request->frame_tree_node() |
| ->frame_tree() |
| .root() |
| ->frame_tree_node_id(); |
| prerender_host = FindNonReservedHostById(main_frame_host_id); |
| } |
| if (prerender_host) { |
| return prerender_host->prerender_host_id(); |
| } |
| return PrerenderHostId(); |
| } |
| |
| void PrerenderHostRegistry::DidStartNavigation( |
| NavigationHandle* navigation_handle) { |
| // DidStartNavigation is used for monitoring the main frame navigation in a |
| // prerendered page so do nothing for other navigations. |
| auto* navigation_request = NavigationRequest::From(navigation_handle); |
| if (!navigation_request->IsInPrerenderedMainFrame() || |
| navigation_request->IsSameDocument()) { |
| return; |
| } |
| |
| // This navigation is running on the main frame in the prerendered page, so |
| // its FrameTree::Delegate should be PrerenderHost. |
| auto& prerender_host = PrerenderHost::GetFromFrameTreeNode( |
| *navigation_request->frame_tree_node()); |
| prerender_host.DidStartNavigation(navigation_handle); |
| } |
| |
| void PrerenderHostRegistry::ReadyToCommitNavigation( |
| NavigationHandle* navigation_handle) { |
| // ReadyToCommitNavigation is used for monitoring the main frame navigation in |
| // a prerendered page so do nothing for other navigations. |
| auto* navigation_request = NavigationRequest::From(navigation_handle); |
| if (!navigation_request->IsInPrerenderedMainFrame() || |
| navigation_request->IsSameDocument()) { |
| return; |
| } |
| |
| // This navigation is running on the main frame in the prerendered page, so |
| // its FrameTree::Delegate should be PrerenderHost. |
| auto& prerender_host = PrerenderHost::GetFromFrameTreeNode( |
| *navigation_request->frame_tree_node()); |
| |
| prerender_host.ReadyToCommitNavigation(navigation_handle); |
| } |
| |
| void PrerenderHostRegistry::DidFinishNavigation( |
| NavigationHandle* navigation_handle) { |
| auto* navigation_request = NavigationRequest::From(navigation_handle); |
| |
| if (navigation_request->IsSameDocument()) |
| return; |
| |
| FrameTreeNodeId main_frame_host_id = navigation_request->frame_tree_node() |
| ->frame_tree() |
| .root() |
| ->frame_tree_node_id(); |
| PrerenderHost* prerender_host = FindNonReservedHostById(main_frame_host_id); |
| if (!prerender_host) |
| return; |
| |
| prerender_host->DidFinishNavigation(navigation_handle); |
| |
| if (running_prerender_host_id_ == main_frame_host_id) { |
| running_prerender_host_id_ = FrameTreeNodeId(); |
| StartPrerendering(FrameTreeNodeId()); |
| } |
| } |
| |
| void PrerenderHostRegistry::OnVisibilityChanged(Visibility visibility) { |
| // Update the timer for prerendering timeout in the background. |
| if (IsBackground(visibility)) { |
| if (timeout_timer_for_embedder_.IsRunning() || |
| timeout_timer_for_speculation_rules_.IsRunning()) { |
| // Keep the timers which started on a previous visibility change. |
| return; |
| } |
| // Keep a prerendered page alive in the background when its visibility |
| // state changes to HIDDEN or OCCLUDED. |
| timeout_timer_for_embedder_.SetTaskRunner(GetTimerTaskRunner()); |
| timeout_timer_for_speculation_rules_.SetTaskRunner(GetTimerTaskRunner()); |
| |
| // Cancel PrerenderHost in the background when it exceeds a certain |
| // amount of time. The timeout differs depending on the trigger type. |
| timeout_timer_for_embedder_.Start( |
| FROM_HERE, kTimeToLiveInBackgroundForEmbedder, |
| base::BindOnce(&PrerenderHostRegistry::CancelHostsForTriggers, |
| base::Unretained(this), |
| std::vector({PreloadingTriggerType::kEmbedder}), |
| PrerenderCancellationReason( |
| PrerenderFinalStatus::kTimeoutBackgrounded))); |
| timeout_timer_for_speculation_rules_.Start( |
| FROM_HERE, kTimeToLiveInBackgroundForSpeculationRules, |
| base::BindOnce( |
| &PrerenderHostRegistry::CancelHostsForTriggers, |
| base::Unretained(this), |
| std::vector( |
| {PreloadingTriggerType::kSpeculationRule, |
| PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld}), |
| PrerenderCancellationReason( |
| PrerenderFinalStatus::kTimeoutBackgrounded))); |
| } else { |
| // Stop the timer when a prerendered page gets visible to users. |
| timeout_timer_for_embedder_.Stop(); |
| timeout_timer_for_speculation_rules_.Stop(); |
| |
| // Start the next prerender if needed. |
| if (running_prerender_host_id_.is_null()) { |
| StartPrerendering(FrameTreeNodeId()); |
| } |
| } |
| } |
| |
| void PrerenderHostRegistry::PrimaryMainFrameRenderProcessGone( |
| base::TerminationStatus status) { |
| CancelAllHosts( |
| status == base::TERMINATION_STATUS_PROCESS_CRASHED |
| ? PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed |
| : PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled); |
| } |
| |
| bool PrerenderHostRegistry::CanNavigationActivateHost( |
| NavigationRequest& navigation_request, |
| PrerenderHost& host) { |
| RenderFrameHostImpl* render_frame_host = |
| navigation_request.frame_tree_node()->current_frame_host(); |
| TRACE_EVENT2("navigation", "PrerenderHostRegistry::CanNavigationActivateHost", |
| "navigation_url", navigation_request.GetURL().spec(), |
| "render_frame_host", render_frame_host); |
| |
| // Disallow activation when the navigation URL has an effective URL like |
| // hosted apps and NTP. |
| if (SiteInstanceImpl::HasEffectiveURL(web_contents()->GetBrowserContext(), |
| navigation_request.GetURL())) { |
| CancelHost(host.frame_tree_node_id(), |
| PrerenderFinalStatus::kActivationUrlHasEffectiveUrl); |
| return false; |
| } |
| |
| // Disallow activation when other auxiliary browsing contexts (e.g., pop-up |
| // windows) exist in the same browsing context group. This is because these |
| // browsing contexts should be able to script each other, but prerendered |
| // pages are created in new browsing context groups. |
| SiteInstance* site_instance = render_frame_host->GetSiteInstance(); |
| if (site_instance->GetRelatedActiveContentsCount() != 1u) { |
| CancelHost(host.frame_tree_node_id(), |
| PrerenderFinalStatus::kActivatedWithAuxiliaryBrowsingContexts); |
| return false; |
| } |
| |
| // TODO(crbug.com/40249964): Remove the restriction after further |
| // investigation and discussion. Disallow activation when the navigation |
| // happens in the hidden tab. |
| if (web_contents()->GetVisibility() == Visibility::HIDDEN && |
| !IsAllowedToActivateInBackgroundForTesting()) { |
| CancelHost(host.frame_tree_node_id(), |
| PrerenderFinalStatus::kActivatedInBackground); |
| return false; |
| } |
| |
| { |
| PrerenderCancellationReason reason = PrerenderCancellationReason:: |
| CreateCandidateReasonForActivationParameterMismatch(); |
| // Compare navigation params from activation with the navigation params |
| // from the initial prerender navigation. If they don't match, the |
| // navigation should not activate the prerendered page. |
| if (!host.AreInitialPrerenderNavigationParamsCompatibleWithNavigation( |
| navigation_request, reason)) { |
| // TODO(lingqi): We'd better cancel all hosts. |
| |
| CancelHost(host.frame_tree_node_id(), reason); |
| return false; |
| } |
| } |
| |
| if (!host.IsFramePolicyCompatibleWithPrimaryFrameTree()) { |
| CancelHost(host.frame_tree_node_id(), |
| PrerenderFinalStatus::kActivationFramePolicyNotCompatible); |
| return false; |
| } |
| |
| // Cancel all the other prerender hosts because we no longer need the other |
| // hosts after we determine the host to be activated. |
| std::vector<FrameTreeNodeId> cancelled_prerenders; |
| for (const auto& [host_id, _] : prerender_host_by_frame_tree_node_id_) { |
| if (host_id != host.frame_tree_node_id()) { |
| cancelled_prerenders.push_back(host_id); |
| } |
| } |
| for (const auto& [host_id, _] : |
| prerender_new_tab_handle_by_frame_tree_node_id_) { |
| cancelled_prerenders.push_back(host_id); |
| } |
| CancelHosts(cancelled_prerenders, |
| PrerenderCancellationReason( |
| PrerenderFinalStatus::kOtherPrerenderedPageActivated)); |
| pending_prerenders_.clear(); |
| |
| return true; |
| } |
| |
| void PrerenderHostRegistry::DeletePendingDeletionHosts( |
| FrameTreeNodeId prerender_host_id) { |
| // Avoid directly destructing the host in the map. See the comments in |
| // `DestructPrerenderHosts()` for details. |
| std::unique_ptr<PrerenderHost> prerender_host = |
| std::move(pending_deletion_hosts_[prerender_host_id]); |
| pending_deletion_hosts_.erase(prerender_host_id); |
| DestructPrerenderHosts(prerender_host); |
| |
| if (pending_deletion_new_tab_prerender_handle_) { |
| // Delete the handle asynchronously to avoid delete `this`, as the handle |
| // owns the prerender WebContents, which indirectly owns this |
| // PrerenderHostRegistry. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon( |
| FROM_HERE, std::move(pending_deletion_new_tab_prerender_handle_)); |
| } |
| } |
| |
| void PrerenderHostRegistry::SchedulePendingDeletionPrerenderNewTabHandle( |
| std::unique_ptr<PrerenderNewTabHandle> handle) { |
| CHECK(!pending_deletion_new_tab_prerender_handle_); |
| pending_deletion_new_tab_prerender_handle_ = std::move(handle); |
| } |
| |
| void PrerenderHostRegistry::ScheduleToDeleteAbandonedHost( |
| std::unique_ptr<PrerenderHost> prerender_host, |
| const PrerenderCancellationReason& cancellation_reason) { |
| prerender_host->RecordFailedFinalStatus(PassKey(), cancellation_reason); |
| if (base::FeatureList::IsEnabled( |
| blink::features::kPageHideEventForPrerender2)) { |
| // TODO(crbug.com/353628449): Support pagehide event dispatch for |
| // PrerenderFinalStatus::kWindowClosed. |
| if (cancellation_reason.final_status() == |
| PrerenderFinalStatus::kSpeculationRuleRemoved) { |
| // Fire unload related events upon intended prerender cancellation. |
| RenderFrameHostImpl* rfhi = prerender_host->GetPrerenderedMainFrameHost(); |
| FrameTreeNodeId prerender_host_id = prerender_host->frame_tree_node_id(); |
| pending_deletion_hosts_[prerender_host_id] = std::move(prerender_host); |
| rfhi->ClosePage(RenderFrameHostImpl::ClosePageSource::kPrerenderDiscard, |
| base::BindRepeating( |
| &PrerenderHostRegistry::DeletePendingDeletionHosts, |
| weak_factory_.GetWeakPtr(), prerender_host_id)); |
| return; |
| } |
| } |
| |
| // Asynchronously delete the prerender host. |
| to_be_deleted_hosts_.push_back(std::move(prerender_host)); |
| |
| // A task has already been scheduled to delete the abandoned hosts. |
| if (to_be_deleted_hosts_.size() > 1) { |
| return; |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&PrerenderHostRegistry::DeleteAbandonedHosts, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PrerenderHostRegistry::DeleteAbandonedHosts() { |
| DestructPrerenderHosts(to_be_deleted_hosts_); |
| } |
| |
| void PrerenderHostRegistry::NotifyTrigger(const GURL& url) { |
| for (Observer& obs : observers_) { |
| obs.OnTrigger(url); |
| } |
| } |
| |
| void PrerenderHostRegistry::NotifyCancel( |
| FrameTreeNodeId host_frame_tree_node_id, |
| const PrerenderCancellationReason& reason) { |
| for (Observer& obs : observers_) { |
| obs.OnCancel(host_frame_tree_node_id, reason); |
| } |
| } |
| |
| PrerenderHostRegistry::PrerenderLimitGroup |
| PrerenderHostRegistry::GetPrerenderLimitGroup( |
| PreloadingTriggerType trigger_type, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness) { |
| switch (trigger_type) { |
| case PreloadingTriggerType::kSpeculationRule: |
| case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld: |
| case PreloadingTriggerType::kSpeculationRuleFromAutoSpeculationRules: |
| CHECK(eagerness.has_value()); |
| return IsImmediateSpeculationEagerness(eagerness.value()) |
| ? PrerenderLimitGroup::kSpeculationRulesImmediate |
| : PrerenderLimitGroup::kSpeculationRulesNonImmediate; |
| case PreloadingTriggerType::kEmbedder: |
| return PrerenderLimitGroup::kEmbedder; |
| } |
| } |
| |
| int PrerenderHostRegistry::GetHostCountByLimitGroup( |
| PrerenderLimitGroup limit_group) { |
| int host_count = 0; |
| for (const auto& [_, host] : prerender_host_by_frame_tree_node_id_) { |
| if (GetPrerenderLimitGroup(host->trigger_type(), host->eagerness()) == |
| limit_group) { |
| ++host_count; |
| } |
| } |
| |
| for (const auto& [_, handle] : |
| prerender_new_tab_handle_by_frame_tree_node_id_) { |
| if (GetPrerenderLimitGroup(handle->trigger_type(), handle->eagerness()) == |
| limit_group) { |
| ++host_count; |
| } |
| } |
| |
| return host_count; |
| } |
| |
| bool PrerenderHostRegistry::IsAllowedToStartPrerenderingForTrigger( |
| PreloadingTriggerType trigger_type, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness) { |
| PrerenderLimitGroup limit_group = |
| GetPrerenderLimitGroup(trigger_type, eagerness); |
| |
| // Apply the limit of maximum number of running prerenders per |
| // PrerenderLimitGroup. |
| switch (limit_group) { |
| case PrerenderLimitGroup::kSpeculationRulesImmediate: { |
| int host_count = GetHostCountByLimitGroup(limit_group); |
| return host_count < kMaxRunningSpeculationRulesImmediatePrerenders; |
| } |
| case PrerenderLimitGroup::kSpeculationRulesNonImmediate: { |
| int host_count = GetHostCountByLimitGroup(limit_group); |
| |
| // When the limit on non-immediate speculation rules is reached, cancel |
| // the oldest host to allow a newly incoming trigger to start. |
| if (host_count >= kMaxRunningSpeculationRulesNonImmediatePrerenders) { |
| FrameTreeNodeId oldest_prerender_host_id; |
| |
| // Find the oldest non-immediate prerender that has not been canceled |
| // yet. |
| do { |
| oldest_prerender_host_id = |
| non_immediate_prerender_host_id_by_arrival_order_.front(); |
| non_immediate_prerender_host_id_by_arrival_order_.pop_front(); |
| } while (!prerender_host_by_frame_tree_node_id_.contains( |
| oldest_prerender_host_id) && |
| !prerender_new_tab_handle_by_frame_tree_node_id_.contains( |
| oldest_prerender_host_id)); |
| |
| CHECK(CancelHost(oldest_prerender_host_id, |
| PrerenderFinalStatus:: |
| kMaxNumOfRunningNonImmediatePrerendersExceeded)); |
| |
| CHECK_LT(GetHostCountByLimitGroup(limit_group), |
| kMaxRunningSpeculationRulesNonImmediatePrerenders); |
| } |
| |
| return true; |
| } |
| case PrerenderLimitGroup::kEmbedder: |
| return IsAllowedToStartPrerenderingForEmbedder(); |
| } |
| } |
| |
| void PrerenderHostRegistry::OnMemoryPressure( |
| base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) { |
| // Ignore the memory pressure event if the memory control is disabled. |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kPrerender2MemoryControls)) { |
| return; |
| } |
| |
| switch (memory_pressure_level) { |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE: |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE: |
| break; |
| case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL: |
| CancelAllHosts(PrerenderFinalStatus::kMemoryPressureAfterTriggered); |
| break; |
| } |
| } |
| |
| scoped_refptr<base::SingleThreadTaskRunner> |
| PrerenderHostRegistry::GetTimerTaskRunner() { |
| return timer_task_runner_for_testing_ |
| ? timer_task_runner_for_testing_ |
| : base::SingleThreadTaskRunner::GetCurrentDefault(); |
| } |
| |
| void PrerenderHostRegistry::SetTaskRunnerForTesting( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner) { |
| timer_task_runner_for_testing_ = std::move(task_runner); |
| } |
| |
| bool PrerenderHostRegistry::PrerenderCanBeStartedWhenInitiatorIsInBackground() { |
| // Allow at most 1 prerendering to be started if the initiator page is |
| // still in the background. |
| |
| // There is a running prerender, so no extra prerender is allowed before |
| // this one is finished. |
| if (running_prerender_host_id_) { |
| return false; |
| } |
| |
| // There are non-pending prerenders, which have finished the initial |
| // navigation and been waiting for activation. Don't start a new prerender. |
| if (prerender_host_by_frame_tree_node_id_.size() - |
| pending_prerenders_.size() >= |
| 1) { |
| return false; |
| } |
| |
| // One or more than prerenders for new tab finished or are running. Don't |
| // start a new prerender. |
| if (prerender_new_tab_handle_by_frame_tree_node_id_.size() >= 1) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool PrerenderHostRegistry::IsAllowedToStartPrerenderingForEmbedder() { |
| int host_count = GetHostCountByLimitGroup(PrerenderLimitGroup::kEmbedder); |
| return web_contents()->GetDelegate()->AllowedPrerenderingCount( |
| *web_contents()) > host_count; |
| } |
| |
| void PrerenderHostRegistry::CancelHostsByOriginFilter( |
| const StoragePartition::StorageKeyMatcherFunction& storage_key_filter, |
| PrerenderFinalStatus final_status) { |
| std::vector<FrameTreeNodeId> ids_to_be_deleted; |
| |
| for (const auto& [host_id, host] : prerender_host_by_frame_tree_node_id_) { |
| if (host->initiator_origin().has_value()) { |
| if (storage_key_filter.Run(blink::StorageKey::CreateFirstParty( |
| host->initiator_origin().value()))) { |
| ids_to_be_deleted.push_back(host_id); |
| } |
| } |
| } |
| |
| for (const auto& [host_id, handle] : |
| prerender_new_tab_handle_by_frame_tree_node_id_) { |
| if (handle->initiator_origin().has_value()) { |
| if (storage_key_filter.Run(blink::StorageKey::CreateFirstParty( |
| handle->initiator_origin().value()))) { |
| ids_to_be_deleted.push_back(host_id); |
| } |
| } else { |
| CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty()); |
| } |
| } |
| |
| if (!ids_to_be_deleted.empty()) { |
| CancelHosts(ids_to_be_deleted, PrerenderCancellationReason(final_status)); |
| } |
| } |
| |
| void PrerenderHostRegistry::RecordPotentialPrerenderProcessReuse( |
| bool kHasMatchableHosts, |
| const GURL& navigation_url) { |
| static constexpr char kPrerenderProcessReuseUMAName[] = |
| "Prerender.Experimental.PrerenderProcessReuseAvailability"; |
| if (kHasMatchableHosts) { |
| base::UmaHistogramEnumeration( |
| kPrerenderProcessReuseUMAName, |
| PrerenderProcessReuseAvailability::kHasMatchableHosts); |
| return; |
| } |
| bool has_same_origin_host = |
| std::find_if(prerender_host_by_frame_tree_node_id_.begin(), |
| prerender_host_by_frame_tree_node_id_.end(), |
| [&navigation_url](const auto& pair) { |
| return pair.second->IsUrlSameOrigin(navigation_url); |
| }) != prerender_host_by_frame_tree_node_id_.end(); |
| bool has_same_site_host = |
| std::find_if(prerender_host_by_frame_tree_node_id_.begin(), |
| prerender_host_by_frame_tree_node_id_.end(), |
| [&navigation_url](const auto& pair) { |
| return pair.second->IsUrlSameSite(navigation_url); |
| }) != prerender_host_by_frame_tree_node_id_.end(); |
| PrerenderProcessReuseAvailability availability = |
| PrerenderProcessReuseAvailability::kNoSameOriginOrSiteHosts; |
| if (has_same_origin_host) { |
| availability = PrerenderProcessReuseAvailability::kHasSameOriginHosts; |
| } else if (has_same_site_host) { |
| availability = PrerenderProcessReuseAvailability::kHasSameSiteHosts; |
| } |
| |
| base::UmaHistogramEnumeration(kPrerenderProcessReuseUMAName, availability); |
| } |
| |
| std::unique_ptr<PrerenderHost> |
| PrerenderHostRegistry::FindAndTakePrerenderHostToReuse( |
| const PrerenderAttributes& attributes) { |
| const GURL prerender_url = attributes.prerendering_url; |
| auto iter = std::find_if(prerender_host_by_frame_tree_node_id_.begin(), |
| prerender_host_by_frame_tree_node_id_.end(), |
| [&prerender_url](const auto& pair) { |
| return pair.second->IsUrlSameSite(prerender_url) && |
| pair.second->IsReusable(); |
| }); |
| if (iter != prerender_host_by_frame_tree_node_id_.end()) { |
| std::unique_ptr<PrerenderHost> reuse_host = std::move(iter->second); |
| prerender_host_by_frame_tree_node_id_.erase(iter); |
| reuse_host->NotifyReused(); |
| return reuse_host; |
| } |
| return nullptr; |
| } |
| |
| } // namespace content |