|  | // Copyright 2015 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "components/page_load_metrics/browser/metrics_web_contents_observer.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <ostream> | 
|  | #include <string> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/location.h" | 
|  | #include "base/memory/ptr_util.h" | 
|  | #include "base/memory/read_only_shared_memory_region.h" | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/observer_list.h" | 
|  | #include "base/time/time.h" | 
|  | #include "base/trace_event/trace_event.h" | 
|  | #include "base/tracing/protos/chrome_track_event.pbzero.h" | 
|  | #include "components/page_load_metrics/browser/metrics_lifecycle_observer.h" | 
|  | #include "components/page_load_metrics/browser/page_load_metrics_embedder_interface.h" | 
|  | #include "components/page_load_metrics/browser/page_load_metrics_memory_tracker.h" | 
|  | #include "components/page_load_metrics/browser/page_load_metrics_update_dispatcher.h" | 
|  | #include "components/page_load_metrics/browser/page_load_metrics_util.h" | 
|  | #include "components/page_load_metrics/browser/page_load_tracker.h" | 
|  | #include "components/page_load_metrics/common/page_load_timing.h" | 
|  | #include "content/public/browser/back_forward_cache.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/cookie_access_details.h" | 
|  | #include "content/public/browser/global_request_id.h" | 
|  | #include "content/public/browser/global_routing_id.h" | 
|  | #include "content/public/browser/media_player_id.h" | 
|  | #include "content/public/browser/navigation_details.h" | 
|  | #include "content/public/browser/navigation_handle.h" | 
|  | #include "content/public/browser/page.h" | 
|  | #include "content/public/browser/render_frame_host.h" | 
|  | #include "content/public/browser/render_process_host.h" | 
|  | #include "content/public/browser/render_view_host.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "content/public/browser/web_contents_observer.h" | 
|  | #include "content/public/browser/web_contents_user_data.h" | 
|  | #include "net/base/ip_endpoint.h" | 
|  | #include "net/base/net_errors.h" | 
|  | #include "net/cookies/canonical_cookie.h" | 
|  | #include "net/http/http_response_headers.h" | 
|  | #include "services/network/public/mojom/fetch_api.mojom-shared.h" | 
|  | #include "third_party/blink/public/common/loader/resource_type_util.h" | 
|  | #include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h" | 
|  | #include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h" | 
|  | #include "ui/base/page_transition_types.h" | 
|  | #include "url/url_constants.h" | 
|  |  | 
|  | namespace page_load_metrics { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // Returns the HTTP status code for the current page, or -1 if no status code | 
|  | // is available. Can only be called if the `navigation_handle` has committed. | 
|  | int GetHttpStatusCode(content::NavigationHandle* navigation_handle) { | 
|  | CHECK(navigation_handle->HasCommitted()); | 
|  | const net::HttpResponseHeaders* response_headers = | 
|  | navigation_handle->GetResponseHeaders(); | 
|  | if (!response_headers) { | 
|  | return -1; | 
|  | } | 
|  | return response_headers->response_code(); | 
|  | } | 
|  |  | 
|  | UserInitiatedInfo CreateUserInitiatedInfo( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | if (!navigation_handle->IsRendererInitiated()) { | 
|  | return UserInitiatedInfo::BrowserInitiated(); | 
|  | } | 
|  |  | 
|  | return UserInitiatedInfo::RenderInitiated( | 
|  | navigation_handle->HasUserGesture(), | 
|  | !navigation_handle->NavigationInputStart().is_null()); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // static | 
|  | void MetricsWebContentsObserver::RecordFeatureUsage( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | const std::vector<blink::mojom::WebFeature>& web_features) { | 
|  | content::WebContents* web_contents = | 
|  | content::WebContents::FromRenderFrameHost(render_frame_host); | 
|  | MetricsWebContentsObserver* observer = | 
|  | MetricsWebContentsObserver::FromWebContents(web_contents); | 
|  |  | 
|  | if (observer) { | 
|  | std::vector<blink::UseCounterFeature> features; | 
|  | for (auto web_feature : web_features) { | 
|  | CHECK_NE(web_feature, blink::mojom::WebFeature::kPageVisits) | 
|  | << "WebFeature::kPageVisits is a reserved feature."; | 
|  | if (web_feature == blink::mojom::WebFeature::kPageVisits) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | features.emplace_back(blink::mojom::UseCounterFeatureType::kWebFeature, | 
|  | static_cast<uint32_t>(web_feature)); | 
|  | } | 
|  | observer->OnBrowserFeatureUsage(render_frame_host, features); | 
|  | } | 
|  | } | 
|  |  | 
|  | // static | 
|  | void MetricsWebContentsObserver::RecordFeatureUsage( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | blink::mojom::WebFeature feature) { | 
|  | MetricsWebContentsObserver::RecordFeatureUsage( | 
|  | render_frame_host, std::vector<blink::mojom::WebFeature>{feature}); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void MetricsWebContentsObserver::RecordFeatureUsage( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | const std::vector<blink::mojom::WebDXFeature>& webdx_features) { | 
|  | content::WebContents* web_contents = | 
|  | content::WebContents::FromRenderFrameHost(render_frame_host); | 
|  | MetricsWebContentsObserver* observer = | 
|  | MetricsWebContentsObserver::FromWebContents(web_contents); | 
|  |  | 
|  | if (observer) { | 
|  | std::vector<blink::UseCounterFeature> features; | 
|  | for (auto webdx_feature : webdx_features) { | 
|  | CHECK_NE(webdx_feature, blink::mojom::WebDXFeature::kPageVisits) | 
|  | << "WebFeature::kPageVisits is a reserved feature."; | 
|  | if (webdx_feature == blink::mojom::WebDXFeature::kPageVisits) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | features.emplace_back(blink::mojom::UseCounterFeatureType::kWebDXFeature, | 
|  | static_cast<uint32_t>(webdx_feature)); | 
|  | } | 
|  | observer->OnBrowserFeatureUsage(render_frame_host, features); | 
|  | } | 
|  | } | 
|  |  | 
|  | // static | 
|  | void MetricsWebContentsObserver::RecordFeatureUsage( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | blink::mojom::WebDXFeature feature) { | 
|  | MetricsWebContentsObserver::RecordFeatureUsage( | 
|  | render_frame_host, std::vector<blink::mojom::WebDXFeature>{feature}); | 
|  | } | 
|  |  | 
|  | // static | 
|  | MetricsWebContentsObserver* MetricsWebContentsObserver::CreateForWebContents( | 
|  | content::WebContents* web_contents, | 
|  | std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface) { | 
|  | CHECK(web_contents); | 
|  |  | 
|  | MetricsWebContentsObserver* metrics = FromWebContents(web_contents); | 
|  | if (!metrics) { | 
|  | metrics = new MetricsWebContentsObserver(web_contents, | 
|  | std::move(embedder_interface)); | 
|  | web_contents->SetUserData(UserDataKey(), base::WrapUnique(metrics)); | 
|  | metrics->created_ = base::TimeTicks::Now(); | 
|  | } | 
|  | return metrics; | 
|  | } | 
|  |  | 
|  | // static | 
|  | void MetricsWebContentsObserver::BindPageLoadMetrics( | 
|  | mojo::PendingAssociatedReceiver<mojom::PageLoadMetrics> receiver, | 
|  | content::RenderFrameHost* rfh) { | 
|  | auto* web_contents = content::WebContents::FromRenderFrameHost(rfh); | 
|  | if (!web_contents) { | 
|  | return; | 
|  | } | 
|  | auto* observer = MetricsWebContentsObserver::FromWebContents(web_contents); | 
|  | if (!observer) { | 
|  | return; | 
|  | } | 
|  | observer->page_load_metrics_receivers_.Bind(rfh, std::move(receiver)); | 
|  | } | 
|  |  | 
|  | MetricsWebContentsObserver::~MetricsWebContentsObserver() = default; | 
|  |  | 
|  | void MetricsWebContentsObserver::WebContentsWillSoonBeDestroyed() { | 
|  | // TODO(crbug.com/40238907): Should not rely on this call. | 
|  | // This method is called only in a certain situation, and most embedders | 
|  | // doesn't support to call this method before WebContentsDestroyed(). | 
|  | web_contents_will_soon_be_destroyed_ = true; | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::WebContentsDestroyed() { | 
|  | // TODO(csharrison): Use a more user-initiated signal for CLOSE. | 
|  | NotifyPageEndAllLoads(END_CLOSE, UserInitiatedInfo::NotUserInitiated()); | 
|  |  | 
|  | // Do this before clearing `primary_page_`, so that the observers don't hit | 
|  | // the CHECK in MetricsWebContentsObserver::GetDelegateForCommittedLoad. | 
|  | for (auto& observer : lifecycle_observers_) { | 
|  | observer.OnGoingAway(); | 
|  | } | 
|  |  | 
|  | UnregisterInputEventObserver(web_contents()->GetPrimaryMainFrame()); | 
|  |  | 
|  | // We tear down PageLoadTrackers in WebContentsDestroyed, rather than in the | 
|  | // destructor, since `web_contents()` returns nullptr in the destructor, and | 
|  | // PageLoadMetricsObservers can cause code to execute that wants to be able to | 
|  | // access the current WebContents. | 
|  | primary_page_ = nullptr; | 
|  | active_pages_.clear(); | 
|  | ukm_dropped_frames_data_.clear(); | 
|  | provisional_loads_.clear(); | 
|  | aborted_provisional_loads_.clear(); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::RegisterInputEventObserver( | 
|  | content::RenderFrameHost* host) { | 
|  | if (host != nullptr) { | 
|  | host->GetRenderWidgetHost()->AddInputEventObserver(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::UnregisterInputEventObserver( | 
|  | content::RenderFrameHost* host) { | 
|  | if (host != nullptr) { | 
|  | host->GetRenderWidgetHost()->RemoveInputEventObserver(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::RenderFrameHostChanged( | 
|  | content::RenderFrameHost* old_host, | 
|  | content::RenderFrameHost* new_host) { | 
|  | if (!new_host->IsInPrimaryMainFrame()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | UnregisterInputEventObserver(old_host); | 
|  | RegisterInputEventObserver(new_host); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::FrameDeleted( | 
|  | content::FrameTreeNodeId frame_tree_node_id) { | 
|  | content::RenderFrameHost* rfh = | 
|  | web_contents()->UnsafeFindFrameByFrameTreeNodeId(frame_tree_node_id); | 
|  | if (!rfh) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Deletion of FrameTreeNode follows deletion of RenderFrameHost. If the node | 
|  | // is root of the page, corresponding PageLoadTracker has gone at this timing. | 
|  | // So, PageLoadTracker cannot forward a deletion event of FrameTreeNode for | 
|  | // itself and MetrcisWebContents does this role. | 
|  | if (PageLoadTracker* tracker = GetAncestralAlivePageLoadTracker(rfh)) { | 
|  | tracker->FrameTreeNodeDeleted(frame_tree_node_id); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::RenderFrameDeleted( | 
|  | content::RenderFrameHost* rfh) { | 
|  | if (auto* memory_tracker = GetMemoryTracker()) { | 
|  | memory_tracker->OnRenderFrameDeleted(rfh, this); | 
|  | } | 
|  |  | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(rfh)) { | 
|  | tracker->RenderFrameDeleted(rfh); | 
|  | } | 
|  |  | 
|  | content::GlobalRenderFrameHostId rfh_id = rfh->GetGlobalId(); | 
|  | auto new_end_it = std::remove_if(queued_memory_updates_.begin(), | 
|  | queued_memory_updates_.end(), | 
|  | [rfh_id](const MemoryUpdate& update) { | 
|  | return update.routing_id == rfh_id; | 
|  | }); | 
|  | queued_memory_updates_.erase(new_end_it, queued_memory_updates_.end()); | 
|  |  | 
|  | // PageLoadTracker and smoothness data can be associated only with a main | 
|  | // frame. | 
|  | if (rfh->GetParent()) { | 
|  | return; | 
|  | } | 
|  | active_pages_.erase(rfh); | 
|  | inactive_pages_.erase(rfh); | 
|  | ukm_dropped_frames_data_.erase(rfh); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::MediaStartedPlaying( | 
|  | const content::WebContentsObserver::MediaPlayerInfo& video_type, | 
|  | const content::MediaPlayerId& id) { | 
|  | auto* render_frame_host = | 
|  | content::RenderFrameHost::FromID(id.frame_routing_id); | 
|  |  | 
|  | // Ignore media that starts playing in a page that was navigated away | 
|  | // from. | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) { | 
|  | tracker->MediaStartedPlaying(video_type, render_frame_host); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::WillStartNavigationRequest( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | // Same-document navigations should never go through | 
|  | // WillStartNavigationRequest. | 
|  | CHECK(!navigation_handle->IsSameDocument()); | 
|  |  | 
|  | if (!navigation_handle->IsInMainFrame()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | WillStartNavigationRequestImpl(navigation_handle); | 
|  | has_navigated_ = true; | 
|  | } | 
|  |  | 
|  | MetricsWebContentsObserver::MetricsWebContentsObserver( | 
|  | content::WebContents* web_contents, | 
|  | std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface) | 
|  | : content::WebContentsObserver(web_contents), | 
|  | content::WebContentsUserData<MetricsWebContentsObserver>(*web_contents), | 
|  | in_foreground_(web_contents->GetVisibility() != | 
|  | content::Visibility::HIDDEN), | 
|  | embedder_interface_(std::move(embedder_interface)), | 
|  | has_navigated_(false), | 
|  | page_load_metrics_receivers_(web_contents, this) { | 
|  | // NoStatePrefetch loads erroneously report that they are initially visible, | 
|  | // so we manually override visibility state for prerender. | 
|  | if (embedder_interface_->IsNoStatePrefetch(web_contents)) { | 
|  | in_foreground_ = false; | 
|  | } | 
|  |  | 
|  | RegisterInputEventObserver(web_contents->GetPrimaryMainFrame()); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::WillStartNavigationRequestImpl( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | UserInitiatedInfo user_initiated_info( | 
|  | CreateUserInitiatedInfo(navigation_handle)); | 
|  | std::unique_ptr<PageLoadTracker> last_aborted = | 
|  | NotifyAbortedProvisionalLoadsNewNavigation(navigation_handle, | 
|  | user_initiated_info); | 
|  |  | 
|  | if (!ShouldTrackMainFrameNavigation(navigation_handle)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Pass in the last committed url to the PageLoadTracker. If the MWCO has | 
|  | // never observed a committed load, use the last committed url from this | 
|  | // WebContent's opener. This is more accurate than using referrers due to | 
|  | // referrer sanitizing and origin referrers. Note that this could potentially | 
|  | // be inaccurate if the opener has since navigated. | 
|  | content::RenderFrameHost* opener = web_contents()->GetOpener(); | 
|  | const GURL& opener_url = | 
|  | !has_navigated_ && opener ? opener->GetLastCommittedURL() : GURL(); | 
|  | const GURL& currently_committed_url = | 
|  | primary_page_ ? primary_page_->url() : opener_url; | 
|  |  | 
|  | bool in_foreground = | 
|  | !navigation_handle->IsInPrerenderedMainFrame() && in_foreground_; | 
|  |  | 
|  | // Prepare ukm::SourceId that is based on outermost page's navigation ID. | 
|  | ukm::SourceId source_id = ukm::kInvalidSourceId; | 
|  | base::WeakPtr<PageLoadTracker> parent_tracker; | 
|  | if (navigation_handle->IsInPrimaryMainFrame()) { | 
|  | // Primary pages use own page's navigation ID. | 
|  | source_id = ukm::ConvertToSourceId(navigation_handle->GetNavigationId(), | 
|  | ukm::SourceIdType::NAVIGATION_ID); | 
|  | } else if (navigation_handle->IsInPrerenderedMainFrame()) { | 
|  | // Prerendering pages should not record UKM until its activation. So, we | 
|  | // start with ukm::kInvalidSourceId and set a correct ukm::SourceId on | 
|  | // activation. | 
|  | CHECK_EQ(ukm::kInvalidSourceId, source_id); | 
|  | } else if (navigation_handle->GetNavigatingFrameType() == | 
|  | content::FrameType::kFencedFrameRoot) { | 
|  | // For FencedFrames, use the primary page's ukm::SourceId. `primary_page_` | 
|  | // can be nullptr if the main frame is in data URL or so. | 
|  | if (primary_page_) { | 
|  | source_id = primary_page_->GetPageUkmSourceId(); | 
|  | parent_tracker = primary_page_->GetWeakPtr(); | 
|  | } else { | 
|  | // Use ukm::NoURLSourceId() rather than kInvalidSourceId to avoid | 
|  | // unexpected check failure. This happens on tests that create a | 
|  | // FencedFrame via FencedFrameTestHelper directly without a correct setup | 
|  | // being finished on the embedder frame. | 
|  | source_id = ukm::NoURLSourceId(); | 
|  | } | 
|  | } else { | 
|  | NOTREACHED(); | 
|  | } | 
|  |  | 
|  | // For prerendered page activations, we don't create a new PageLoadTracker, | 
|  | // but reuse an existing one that was created for the initial prerendering | 
|  | // navigation so that the same instance will bee OnPrerenderStart and | 
|  | // DidActivatePrerenderedPage. | 
|  | if (navigation_handle->IsPrerenderedPageActivation()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Passing raw pointers to `embedder_interface_` is safe because the | 
|  | // MetricsWebContentsObserver owns them both list and they are torn down after | 
|  | // the PageLoadTracker. The PageLoadTracker does not hold on to | 
|  | // `navigation_handle` beyond the scope of the constructor. | 
|  | auto insertion_result = provisional_loads_.insert(std::make_pair( | 
|  | navigation_handle, | 
|  | std::make_unique<PageLoadTracker>( | 
|  | PageLoadTracker::InForegroundBool{in_foreground}, | 
|  | embedder_interface_.get(), currently_committed_url, | 
|  | PageLoadTracker::IsFirstNavigationInWebContentsBool{!has_navigated_}, | 
|  | PageLoadTracker::IsReloadAfterDiscardBool{ | 
|  | navigation_handle->ExistingDocumentWasDiscarded()}, | 
|  | navigation_handle, user_initiated_info, source_id, parent_tracker))); | 
|  | CHECK(insertion_result.second) | 
|  | << "provisional_loads_ already contains NavigationHandle."; | 
|  | for (auto& observer : lifecycle_observers_) { | 
|  | observer.OnTrackerCreated(insertion_result.first->second.get()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::WillProcessNavigationResponse( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | auto it = provisional_loads_.find(navigation_handle); | 
|  | if (it == provisional_loads_.end()) { | 
|  | return; | 
|  | } | 
|  | it->second->WillProcessNavigationResponse(navigation_handle); | 
|  | } | 
|  |  | 
|  | PageLoadTracker* MetricsWebContentsObserver::GetTrackerOrNullForRequest( | 
|  | const content::GlobalRequestID& request_id, | 
|  | content::RenderFrameHost* render_frame_host_or_null, | 
|  | network::mojom::RequestDestination request_destination, | 
|  | base::TimeTicks creation_time) { | 
|  | if (request_destination == network::mojom::RequestDestination::kDocument) { | 
|  | CHECK(request_id != content::GlobalRequestID()); | 
|  | // The main frame request can complete either before or after commit, so we | 
|  | // look at both provisional loads and the committed load to find a | 
|  | // PageLoadTracker with a matching request id. See https://goo.gl/6TzCYN for | 
|  | // more details. | 
|  | for (const auto& kv : provisional_loads_) { | 
|  | PageLoadTracker* candidate = kv.second.get(); | 
|  | if (candidate->HasMatchingNavigationRequestID(request_id)) { | 
|  | return candidate; | 
|  | } | 
|  | } | 
|  | if (primary_page_ && | 
|  | primary_page_->HasMatchingNavigationRequestID(request_id)) { | 
|  | return primary_page_.get(); | 
|  | } | 
|  | if (auto page_pair = inactive_pages_.find(render_frame_host_or_null); | 
|  | page_pair != inactive_pages_.end()) { | 
|  | return page_pair->second.get(); | 
|  | } | 
|  | } else { | 
|  | // Non main resources are always associated with the currently committed | 
|  | // load, `primary_page_` or `active_pages_`. If the resource | 
|  | // request was started before this navigation of them, then it should be | 
|  | // ignored. Check `primary_page_` here as its start time is the oldest one. | 
|  | if (!primary_page_ || creation_time < primary_page_->navigation_start()) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | // Sub-frame resources have a null RFH when browser-side navigation is | 
|  | // enabled, so we can't perform the RFH check below for them. | 
|  | // | 
|  | // TODO(crbug.com/40216775): consider tracking GlobalRequestIDs for | 
|  | // sub-frame navigations in each PageLoadTracker, and performing a lookup | 
|  | // for sub-frames similar to the main-frame lookup above. Now we have | 
|  | // `active_pages_` in addition to `primary_page_`, and the following code | 
|  | // cannot handle sub-frames inside FencedFrames. | 
|  | if (blink::IsRequestDestinationFrame(request_destination)) { | 
|  | return primary_page_.get(); | 
|  | } | 
|  |  | 
|  | // This was originally a CHECK but it fails when the document load happened | 
|  | // after client certificate selection. | 
|  | if (!render_frame_host_or_null) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | // There is a race here: a completed resource for the previously committed | 
|  | // page can arrive after the new page has committed. In this case, we may | 
|  | // attribute the resource to the wrong page load. We do our best to guard | 
|  | // against this by verifying that the RFH for the resource matches the RFH | 
|  | // for the currently committed load, however there are cases where the same | 
|  | // RFH is used across page loads (same origin navigations, as well as some | 
|  | // cross-origin render-initiated navigations). | 
|  | // | 
|  | // TODO(crbug.com/40528374): use a DocumentId here instead, to eliminate | 
|  | // this race. | 
|  | return GetPageLoadTracker(render_frame_host_or_null); | 
|  | } | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::ResourceLoadComplete( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | const content::GlobalRequestID& request_id, | 
|  | const blink::mojom::ResourceLoadInfo& resource_load_info) { | 
|  | if (!ShouldTrackScheme(resource_load_info.final_url.scheme())) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | PageLoadTracker* tracker = GetTrackerOrNullForRequest( | 
|  | request_id, render_frame_host, resource_load_info.request_destination, | 
|  | resource_load_info.load_timing_info.request_start); | 
|  | if (tracker) { | 
|  | // TODO(crbug.com/41318940): Fill in data reduction proxy fields when this | 
|  | // is available in the network service. int original_content_length = | 
|  | //     was_cached ? 0 | 
|  | //                : data_reduction_proxy::util::EstimateOriginalBodySize( | 
|  | //                      request, lofi_decider); | 
|  | base::ByteCount original_content_length; | 
|  |  | 
|  | const blink::mojom::CommonNetworkInfoPtr& network_info = | 
|  | resource_load_info.network_info; | 
|  | ExtraRequestCompleteInfo extra_request_complete_info( | 
|  | url::SchemeHostPort(resource_load_info.final_url), | 
|  | network_info->remote_endpoint.value(), | 
|  | render_frame_host->GetFrameTreeNodeId(), resource_load_info.was_cached, | 
|  | resource_load_info.raw_body_bytes, original_content_length, | 
|  | resource_load_info.request_destination, resource_load_info.net_error, | 
|  | std::make_unique<net::LoadTimingInfo>( | 
|  | resource_load_info.load_timing_info)); | 
|  | tracker->OnLoadedResource(extra_request_complete_info); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::FrameReceivedUserActivation( | 
|  | content::RenderFrameHost* render_frame_host) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) { | 
|  | tracker->FrameReceivedUserActivation(render_frame_host); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::FrameDisplayStateChanged( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | bool is_display_none) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) { | 
|  | tracker->FrameDisplayStateChanged(render_frame_host, is_display_none); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::FrameSizeChanged( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | const gfx::Size& frame_size) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) { | 
|  | tracker->FrameSizeChanged(render_frame_host, frame_size); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnCookiesAccessed( | 
|  | content::NavigationHandle* navigation, | 
|  | const content::CookieAccessDetails& details) { | 
|  | PageLoadTracker* tracker = nullptr; | 
|  | if (navigation->GetParentFrame()) { | 
|  | // For subframe navigations, notify the main frame's tracker. | 
|  | tracker = GetPageLoadTracker(navigation->GetParentFrame()); | 
|  | } else { | 
|  | // For uncommitted main frame navigations, find a tracker from | 
|  | // `provisional_loads_`. | 
|  | auto it = provisional_loads_.find(navigation); | 
|  | if (it != provisional_loads_.end()) { | 
|  | tracker = it->second.get(); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (tracker) { | 
|  | OnCookiesAccessedImpl(*tracker, details); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnCookiesAccessed( | 
|  | content::RenderFrameHost* rfh, | 
|  | const content::CookieAccessDetails& details) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(rfh)) { | 
|  | OnCookiesAccessedImpl(*tracker, details); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnCookiesAccessedImpl( | 
|  | PageLoadTracker& tracker, | 
|  | const content::CookieAccessDetails& details) { | 
|  | // TODO(altimin): Propagate `CookieAccessDetails` further. | 
|  | bool is_partitioned_access = std::ranges::all_of( | 
|  | details.cookie_access_result_list, | 
|  | [](const net::CookieWithAccessResult& cookie_with_access_result) { | 
|  | return cookie_with_access_result.cookie.IsPartitioned(); | 
|  | }); | 
|  |  | 
|  | switch (details.type) { | 
|  | case content::CookieAccessDetails::Type::kRead: | 
|  | tracker.OnCookiesRead(details.url, details.first_party_url, | 
|  | details.blocked_by_policy, details.is_ad_tagged, | 
|  | details.cookie_setting_overrides, | 
|  | is_partitioned_access); | 
|  | break; | 
|  | case content::CookieAccessDetails::Type::kChange: | 
|  | for (const auto& cookie_with_access_result : | 
|  | details.cookie_access_result_list) { | 
|  | tracker.OnCookieChange(details.url, details.first_party_url, | 
|  | cookie_with_access_result.cookie, | 
|  | details.blocked_by_policy, details.is_ad_tagged, | 
|  | details.cookie_setting_overrides, | 
|  | is_partitioned_access); | 
|  | } | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::DidActivatePreviewedPage( | 
|  | base::TimeTicks activation_time) { | 
|  | // TODO(b:334709645): Investigate how nullptr cases happen. | 
|  | if (primary_page_) { | 
|  | primary_page_->DidActivatePreviewedPage(activation_time); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnStorageAccessed( | 
|  | content::RenderFrameHost* rfh, | 
|  | const GURL& url, | 
|  | const GURL& first_party_url, | 
|  | bool blocked_by_policy, | 
|  | StorageType storage_type) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(rfh)) { | 
|  | tracker->OnStorageAccessed(url, first_party_url, blocked_by_policy, | 
|  | storage_type); | 
|  | } | 
|  | } | 
|  |  | 
|  | const PageLoadMetricsObserverDelegate& | 
|  | MetricsWebContentsObserver::GetDelegateForCommittedLoad() { | 
|  | CHECK(primary_page_); | 
|  | return *primary_page_.get(); | 
|  | } | 
|  |  | 
|  | const PageLoadMetricsObserverDelegate* | 
|  | MetricsWebContentsObserver::GetDelegateForCommittedLoadOrNull() { | 
|  | return primary_page_.get(); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::ReadyToCommitNavigation( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | if (navigation_handle->IsInPrimaryMainFrame()) { | 
|  | // Notify `primary_page_` that we are ready to commit a navigation to a | 
|  | // new page in the primary main frame. | 
|  | if (primary_page_) { | 
|  | primary_page_->ReadyToCommitNavigation(navigation_handle); | 
|  | } | 
|  | } else if (navigation_handle->IsInMainFrame()) { | 
|  | // For non-primary main frame, we notify the PageLoadTracker associated with | 
|  | // the RenderFrameHost that triggers the navigation. | 
|  | PageLoadTracker* tracker = | 
|  | GetPageLoadTracker(navigation_handle->GetRenderFrameHost()); | 
|  | if (tracker) { | 
|  | tracker->ReadyToCommitNavigation(navigation_handle); | 
|  | } | 
|  | } else { | 
|  | // For subframe navigations, notify the PageLoadTracker associated with the | 
|  | // main frame. | 
|  | PageLoadTracker* tracker = | 
|  | GetPageLoadTracker(navigation_handle->GetParentFrame()); | 
|  | if (tracker) { | 
|  | tracker->ReadyToCommitNavigation(navigation_handle); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::DidFinishNavigation( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | if (!navigation_handle->IsInMainFrame()) { | 
|  | PageLoadTracker* tracker = | 
|  | GetPageLoadTracker(navigation_handle->GetParentFrame()); | 
|  | if (tracker) { | 
|  | tracker->DidFinishSubFrameNavigation(navigation_handle); | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | CHECK(navigation_handle->IsInMainFrame()); | 
|  | // Not all navigations trigger the WillStartNavigationRequest callback (for | 
|  | // example, navigations to about:blank). DidFinishNavigation is guaranteed to | 
|  | // be called for every navigation, so we also update has_navigated_ here, to | 
|  | // ensure it is set consistently for all navigations. | 
|  | // TODO(crbug.com/40216775): This flag seems broken for Prerender and | 
|  | // FencedFrames. | 
|  | has_navigated_ = true; | 
|  | main_frame_is_webui_ = web_contents()->GetWebUI() != nullptr; | 
|  |  | 
|  | std::unique_ptr<PageLoadTracker> navigation_handle_tracker( | 
|  | std::move(provisional_loads_[navigation_handle])); | 
|  | provisional_loads_.erase(navigation_handle); | 
|  |  | 
|  | if (navigation_handle->HasCommitted() && | 
|  | navigation_handle->IsSameDocument()) { | 
|  | if (navigation_handle_tracker) { | 
|  | navigation_handle_tracker->StopTracking(); | 
|  | } | 
|  | if (navigation_handle->IsInPrimaryMainFrame()) { | 
|  | if (primary_page_) { | 
|  | primary_page_->DidCommitSameDocumentNavigation(navigation_handle); | 
|  | } | 
|  | } else { | 
|  | // Handle the event for non-primary main frames, i.e., FencedFrames. | 
|  | PageLoadTracker* tracker = | 
|  | GetPageLoadTracker(navigation_handle->GetRenderFrameHost()); | 
|  | if (tracker) { | 
|  | tracker->DidCommitSameDocumentNavigation(navigation_handle); | 
|  | } | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Ignore internally generated aborts for navigations with HTTP responses that | 
|  | // don't commit, such as HTTP 204 responses and downloads. | 
|  | if (!navigation_handle->HasCommitted() && | 
|  | navigation_handle->GetNetErrorCode() == net::ERR_ABORTED && | 
|  | navigation_handle->GetResponseHeaders()) { | 
|  | if (navigation_handle_tracker) { | 
|  | navigation_handle_tracker->DidInternalNavigationAbort(navigation_handle); | 
|  | navigation_handle_tracker->StopTracking(); | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (navigation_handle->HasCommitted() && | 
|  | navigation_handle->IsInPrimaryMainFrame()) { | 
|  | // A new navigation is committing, so finalize and destroy the tracker for | 
|  | // the currently committed navigation. | 
|  | FinalizeCurrentlyCommittedLoad(navigation_handle, | 
|  | navigation_handle_tracker.get()); | 
|  |  | 
|  | if (primary_page_) { | 
|  | // Mark the current tracker as it sees a link navigation. | 
|  | ui::PageTransition transition = navigation_handle->GetPageTransition(); | 
|  | if (ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_LINK)) { | 
|  | primary_page_->RecordLinkNavigation(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Transfers the ownership of `primary_page_`. This `primary_page_` | 
|  | // might be reused later when restoring the page from the cache. | 
|  | // Note: back-forward cache doesn't support features that rely on | 
|  | // `active_pages_`, such as FencedFrames. | 
|  | MaybeStorePageLoadTrackerForBackForwardCache(navigation_handle, | 
|  | std::move(primary_page_)); | 
|  |  | 
|  | // If `navigation_handle` already has an associated PageLoadTracker in | 
|  | // `inactive_pages_`, move it into `primary_page_`. | 
|  | if (MaybeActivatePageLoadTracker(navigation_handle)) { | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!navigation_handle_tracker) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!ShouldTrackMainFrameNavigation(navigation_handle)) { | 
|  | navigation_handle_tracker->StopTracking(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (navigation_handle->HasCommitted()) { | 
|  | navigation_handle_tracker->SetPageMainFrame( | 
|  | navigation_handle->GetRenderFrameHost()); | 
|  | HandleCommittedNavigationForTrackedLoad( | 
|  | navigation_handle, std::move(navigation_handle_tracker)); | 
|  | } else { | 
|  | HandleFailedNavigationForTrackedLoad(navigation_handle, | 
|  | std::move(navigation_handle_tracker)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Handle a pre-commit error. Navigations that result in an error page will be | 
|  | // ignored. | 
|  | void MetricsWebContentsObserver::HandleFailedNavigationForTrackedLoad( | 
|  | content::NavigationHandle* navigation_handle, | 
|  | std::unique_ptr<PageLoadTracker> tracker) { | 
|  | const base::TimeTicks now = base::TimeTicks::Now(); | 
|  | tracker->FailedProvisionalLoad(navigation_handle, now); | 
|  |  | 
|  | const net::Error error = navigation_handle->GetNetErrorCode(); | 
|  |  | 
|  | // net::OK: This case occurs when the NavigationHandle finishes and reports | 
|  | // !HasCommitted(), but reports no net::Error. This represents the navigation | 
|  | // being stopped by the user before it was ready to commit. | 
|  | // net::ERR_ABORTED: An aborted provisional load has error net::ERR_ABORTED. | 
|  | const bool is_aborted_provisional_load = | 
|  | error == net::OK || error == net::ERR_ABORTED; | 
|  |  | 
|  | // If is_aborted_provisional_load, the page end reason is not yet known, and | 
|  | // will be updated as additional information is available from subsequent | 
|  | // navigations. | 
|  | tracker->NotifyPageEnd( | 
|  | is_aborted_provisional_load ? END_OTHER : END_PROVISIONAL_LOAD_FAILED, | 
|  | UserInitiatedInfo::NotUserInitiated(), now, true); | 
|  |  | 
|  | if (is_aborted_provisional_load) { | 
|  | aborted_provisional_loads_.push_back(std::move(tracker)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::HandleCommittedNavigationForTrackedLoad( | 
|  | content::NavigationHandle* navigation_handle, | 
|  | std::unique_ptr<PageLoadTracker> tracker) { | 
|  | PageLoadTracker* raw_tracker = tracker.get(); | 
|  | if (navigation_handle->IsInPrerenderedMainFrame()) { | 
|  | // The PageLoadTracker already exists when a main frame navigation after the | 
|  | // initial prerener navigation in a prerendering page is finished. Replace | 
|  | // the old page tracker with the new one. | 
|  | if (auto existing_tracker_iter = | 
|  | inactive_pages_.find(navigation_handle->GetRenderFrameHost()); | 
|  | existing_tracker_iter != inactive_pages_.end()) { | 
|  | inactive_pages_.erase(existing_tracker_iter); | 
|  | } | 
|  | inactive_pages_.emplace(navigation_handle->GetRenderFrameHost(), | 
|  | std::move(tracker)); | 
|  | } else if (navigation_handle->IsInPrimaryMainFrame()) { | 
|  | primary_page_ = std::move(tracker); | 
|  | active_pages_.clear(); | 
|  | } else { | 
|  | CHECK_EQ(navigation_handle->GetNavigatingFrameType(), | 
|  | content::FrameType::kFencedFrameRoot); | 
|  | // There may be an active tracker in the map if navigation happens on the | 
|  | // non-primary page. `emplace` operation below doesn't overwrite it, but | 
|  | // just fails. It results in destructing the moved tracker unexpectedly. | 
|  | // To avoid this problem, we ensure destructing existing tracker beforehand. | 
|  | auto it = active_pages_.find(navigation_handle->GetRenderFrameHost()); | 
|  | if (it != active_pages_.end()) { | 
|  | active_pages_.erase(it); | 
|  | } | 
|  |  | 
|  | active_pages_.emplace(navigation_handle->GetRenderFrameHost(), | 
|  | std::move(tracker)); | 
|  | } | 
|  | raw_tracker->Commit(navigation_handle); | 
|  | CHECK(raw_tracker->did_commit()); | 
|  |  | 
|  | for (auto& observer : lifecycle_observers_) { | 
|  | observer.OnCommit(raw_tracker); | 
|  | } | 
|  |  | 
|  | auto* render_frame_host = navigation_handle->GetRenderFrameHost(); | 
|  | const bool is_main_frame = | 
|  | render_frame_host && render_frame_host->GetParent() == nullptr; | 
|  | if (is_main_frame) { | 
|  | auto ukm_it = ukm_dropped_frames_data_.find(render_frame_host); | 
|  | if (ukm_it != ukm_dropped_frames_data_.end()) { | 
|  | raw_tracker->metrics_update_dispatcher() | 
|  | ->SetUpSharedMemoryForDroppedFrames(render_frame_host, | 
|  | std::move(ukm_it->second)); | 
|  | ukm_dropped_frames_data_.erase(ukm_it); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Send queued memory updates for the tracker. | 
|  | content::GlobalRenderFrameHostId rfh_id = render_frame_host->GetGlobalId(); | 
|  | auto first_update_for_rfh = std::partition( | 
|  | queued_memory_updates_.begin(), queued_memory_updates_.end(), | 
|  | [rfh_id](const MemoryUpdate& update) { | 
|  | return update.routing_id != rfh_id; | 
|  | }); | 
|  | if (first_update_for_rfh != queued_memory_updates_.end()) { | 
|  | raw_tracker->OnV8MemoryChanged(std::vector<MemoryUpdate>( | 
|  | first_update_for_rfh, queued_memory_updates_.end())); | 
|  | queued_memory_updates_.erase(first_update_for_rfh, | 
|  | queued_memory_updates_.end()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::MaybeStorePageLoadTrackerForBackForwardCache( | 
|  | content::NavigationHandle* next_navigation_handle, | 
|  | std::unique_ptr<PageLoadTracker> previously_committed_load) { | 
|  | TRACE_EVENT1("loading", | 
|  | "MetricsWebContentsObserver::" | 
|  | "MaybeRestorePageLoadTrackerForBackForwardCache", | 
|  | "next_navigation", next_navigation_handle); | 
|  |  | 
|  | if (!previously_committed_load) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | content::RenderFrameHost* previous_frame = content::RenderFrameHost::FromID( | 
|  | next_navigation_handle->GetPreviousRenderFrameHostId()); | 
|  |  | 
|  | // The PageLoadTracker is associated with a bfcached document if: | 
|  | bool is_back_forward_cache = | 
|  | // 1. the frame being navigated away from was not already deleted | 
|  | previous_frame && | 
|  | // 2. the previous frame is in the BFCache | 
|  | (previous_frame->GetLifecycleState() == | 
|  | content::RenderFrameHost::LifecycleState::kInBackForwardCache); | 
|  |  | 
|  | if (!is_back_forward_cache) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | previously_committed_load->OnEnterBackForwardCache(); | 
|  | inactive_pages_.emplace(previous_frame, std::move(previously_committed_load)); | 
|  | for (auto& kv : active_pages_) { | 
|  | kv.second->OnEnterBackForwardCache(); | 
|  | inactive_pages_.emplace(kv.first, std::move(kv.second)); | 
|  | } | 
|  | active_pages_.clear(); | 
|  | } | 
|  |  | 
|  | bool MetricsWebContentsObserver::MaybeActivatePageLoadTracker( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | TRACE_EVENT1("loading", | 
|  | "MetricsWebContentsObserver::" | 
|  | "MaybeActivatePageLoadTracker", | 
|  | "navigation", navigation_handle); | 
|  |  | 
|  | auto it = inactive_pages_.find(navigation_handle->GetRenderFrameHost()); | 
|  |  | 
|  | // There are some cases that the PageLoadTracker does not exist even if | 
|  | // `navigation_handle` is served from the back/forward cache. For example, | 
|  | // if a page is put into the cache before MetricsWebContents is created, | 
|  | // `inactive_pages_` is empty. | 
|  | if (it == inactive_pages_.end()) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | active_pages_.clear(); | 
|  |  | 
|  | // This should be a back/forward cache or prerender navigation if we find | 
|  | // an inactive_page. | 
|  | CHECK(navigation_handle->IsServedFromBackForwardCache() || | 
|  | navigation_handle->IsPrerenderedPageActivation()); | 
|  |  | 
|  | auto* primary_main_frame = navigation_handle->GetRenderFrameHost(); | 
|  | primary_main_frame->ForEachRenderFrameHost( | 
|  | [&](content::RenderFrameHost* rfh) { | 
|  | // Skip RenderFrameHosts that aren't main frames. | 
|  | if (rfh != rfh->GetMainFrame()) { | 
|  | return; | 
|  | } | 
|  | auto it = inactive_pages_.find(rfh); | 
|  | if (it == inactive_pages_.end()) { | 
|  | return; | 
|  | } | 
|  | PageLoadTracker* tracker; | 
|  | if (rfh == primary_main_frame) { | 
|  | primary_page_ = std::move(it->second); | 
|  | tracker = primary_page_.get(); | 
|  | } else { | 
|  | tracker = active_pages_.emplace(it->first, std::move(it->second)) | 
|  | .first->second.get(); | 
|  | } | 
|  | inactive_pages_.erase(it); | 
|  | if (navigation_handle->IsServedFromBackForwardCache()) { | 
|  | tracker->OnRestoreFromBackForwardCache(navigation_handle); | 
|  | } else if (navigation_handle->IsPrerenderedPageActivation()) { | 
|  | tracker->DidActivatePrerenderedPage(navigation_handle); | 
|  | } | 
|  | }); | 
|  |  | 
|  | for (auto& observer : lifecycle_observers_) { | 
|  | observer.OnActivate(primary_page_.get()); | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::FinalizeCurrentlyCommittedLoad( | 
|  | content::NavigationHandle* newly_committed_navigation, | 
|  | PageLoadTracker* newly_committed_navigation_tracker) { | 
|  | UserInitiatedInfo user_initiated_info = | 
|  | newly_committed_navigation_tracker | 
|  | ? newly_committed_navigation_tracker->user_initiated_info() | 
|  | : CreateUserInitiatedInfo(newly_committed_navigation); | 
|  |  | 
|  | // Notify other loads that they may have been aborted by this committed | 
|  | // load. is_certainly_browser_timestamp is set to false because | 
|  | // NavigationStart() could be set in either the renderer or browser process. | 
|  | NotifyPageEndAllLoadsWithTimestamp( | 
|  | EndReasonForPageTransition( | 
|  | newly_committed_navigation->GetPageTransition()), | 
|  | user_initiated_info, newly_committed_navigation->NavigationStart(), | 
|  | /*is_certainly_browser_timestamp=*/false); | 
|  |  | 
|  | if (primary_page_) { | 
|  | // Ensure that any pending update gets dispatched. | 
|  | primary_page_->metrics_update_dispatcher()->FlushPendingTimingUpdates(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::NavigationStopped() { | 
|  | // TODO(csharrison): Use a more user-initiated signal for STOP. | 
|  | NotifyPageEndAllLoads(END_STOP, UserInitiatedInfo::NotUserInitiated()); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnInputEvent( | 
|  | const content::RenderWidgetHost& widget, | 
|  | const blink::WebInputEvent& event) { | 
|  | // Ignore browser navigation or reload which comes with type Undefined. | 
|  | if (event.GetType() == blink::WebInputEvent::Type::kUndefined) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // For now, we assume input events occur only in primary page. | 
|  | if (primary_page_) { | 
|  | primary_page_->OnInputEvent(event); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::FlushMetricsOnAppEnterBackground() { | 
|  | // Note that, while a call to FlushMetricsOnAppEnterBackground usually | 
|  | // indicates that the app is about to be backgrounded, there are cases where | 
|  | // the app may not end up getting backgrounded. Thus, we should not assume | 
|  | // anything about foreground / background state of the associated tab as part | 
|  | // of this method call. | 
|  |  | 
|  | if (primary_page_) { | 
|  | primary_page_->FlushMetricsOnAppEnterBackground(); | 
|  | } | 
|  | for (const auto& kv : active_pages_) { | 
|  | kv.second->FlushMetricsOnAppEnterBackground(); | 
|  | } | 
|  | for (const auto& kv : inactive_pages_) { | 
|  | kv.second->FlushMetricsOnAppEnterBackground(); | 
|  | } | 
|  | for (const auto& kv : provisional_loads_) { | 
|  | kv.second->FlushMetricsOnAppEnterBackground(); | 
|  | } | 
|  | for (const auto& tracker : aborted_provisional_loads_) { | 
|  | tracker->FlushMetricsOnAppEnterBackground(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::DidRedirectNavigation( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | if (!navigation_handle->IsInMainFrame()) { | 
|  | return; | 
|  | } | 
|  | auto it = provisional_loads_.find(navigation_handle); | 
|  | if (it == provisional_loads_.end()) { | 
|  | return; | 
|  | } | 
|  | it->second->Redirect(navigation_handle); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::DidUpdateNavigationHandleTiming( | 
|  | content::NavigationHandle* navigation_handle) { | 
|  | if (!navigation_handle->IsInMainFrame()) { | 
|  | return; | 
|  | } | 
|  | auto it = provisional_loads_.find(navigation_handle); | 
|  | if (it == provisional_loads_.end()) { | 
|  | return; | 
|  | } | 
|  | it->second->DidUpdateNavigationHandleTiming(navigation_handle); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnVisibilityChanged( | 
|  | content::Visibility visibility) { | 
|  | if (web_contents_will_soon_be_destroyed_) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | bool was_in_foreground = in_foreground_; | 
|  | in_foreground_ = visibility == content::Visibility::VISIBLE; | 
|  | if (in_foreground_ == was_in_foreground) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (in_foreground_) { | 
|  | if (primary_page_) { | 
|  | primary_page_->PageShown(); | 
|  | } | 
|  | for (const auto& kv : active_pages_) { | 
|  | kv.second->PageShown(); | 
|  | } | 
|  | for (const auto& kv : provisional_loads_) { | 
|  | // Prerendered pages are always invisible regardless of the WebContents' | 
|  | // visibility status. | 
|  | if (!kv.first->IsInPrerenderedMainFrame()) { | 
|  | kv.second->PageShown(); | 
|  | } | 
|  | } | 
|  | } else { | 
|  | if (primary_page_) { | 
|  | primary_page_->PageHidden(); | 
|  | } | 
|  | for (const auto& kv : active_pages_) { | 
|  | kv.second->PageHidden(); | 
|  | } | 
|  | for (const auto& kv : provisional_loads_) { | 
|  | if (!kv.first->IsInPrerenderedMainFrame()) { | 
|  | kv.second->PageHidden(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // As pages in back-forward cache are frozen and prerendered pages are always | 
|  | // invisible, `inactive_pages_` don't have to be iterated here. | 
|  | } | 
|  |  | 
|  | // This will occur when the process for the main RenderFrameHost exits, either | 
|  | // normally or from a crash. We eagerly log data from the last committed load if | 
|  | // we have one. | 
|  | void MetricsWebContentsObserver::PrimaryMainFrameRenderProcessGone( | 
|  | base::TerminationStatus status) { | 
|  | // Other code paths will be run for normal renderer shutdown. Note that we | 
|  | // sometimes get the STILL_RUNNING value on fast shutdown. | 
|  | if (status == base::TERMINATION_STATUS_NORMAL_TERMINATION || | 
|  | status == base::TERMINATION_STATUS_STILL_RUNNING) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // RenderProcessGone is associated with the RenderFrameHost for the | 
|  | // currently committed load. We don't know if the pending navs or aborted | 
|  | // pending navs are associated w/ the render process that died, so we can't be | 
|  | // sure the info should propagate to them. | 
|  | const auto now = base::TimeTicks::Now(); | 
|  | if (primary_page_) { | 
|  | primary_page_->NotifyPageEnd(END_RENDER_PROCESS_GONE, | 
|  | UserInitiatedInfo::NotUserInitiated(), now, | 
|  | true); | 
|  | } | 
|  | for (const auto& kv : active_pages_) { | 
|  | kv.second->NotifyPageEnd(END_RENDER_PROCESS_GONE, | 
|  | UserInitiatedInfo::NotUserInitiated(), now, true); | 
|  | } | 
|  |  | 
|  | // If this is a crash, eagerly log the aborted provisional loads and the | 
|  | // committed load. `provisional_loads_` don't need to be destroyed here | 
|  | // because their lifetime is tied to the NavigationHandle. | 
|  | primary_page_.reset(); | 
|  | active_pages_.clear(); | 
|  | aborted_provisional_loads_.clear(); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::NotifyPageEndAllLoads( | 
|  | PageEndReason page_end_reason, | 
|  | UserInitiatedInfo user_initiated_info) { | 
|  | NotifyPageEndAllLoadsWithTimestamp(page_end_reason, user_initiated_info, | 
|  | base::TimeTicks::Now(), | 
|  | /*is_certainly_browser_timestamp=*/true); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::NotifyPageEndAllLoadsWithTimestamp( | 
|  | PageEndReason page_end_reason, | 
|  | UserInitiatedInfo user_initiated_info, | 
|  | base::TimeTicks timestamp, | 
|  | bool is_certainly_browser_timestamp) { | 
|  | if (primary_page_) { | 
|  | primary_page_->NotifyPageEnd(page_end_reason, user_initiated_info, | 
|  | timestamp, is_certainly_browser_timestamp); | 
|  | } | 
|  | for (const auto& kv : active_pages_) { | 
|  | kv.second->NotifyPageEnd(page_end_reason, user_initiated_info, timestamp, | 
|  | is_certainly_browser_timestamp); | 
|  | } | 
|  | for (const auto& kv : provisional_loads_) { | 
|  | kv.second->NotifyPageEnd(page_end_reason, user_initiated_info, timestamp, | 
|  | is_certainly_browser_timestamp); | 
|  | } | 
|  | for (const auto& tracker : aborted_provisional_loads_) { | 
|  | if (tracker->IsLikelyProvisionalAbort(timestamp)) { | 
|  | tracker->UpdatePageEnd(page_end_reason, user_initiated_info, timestamp, | 
|  | is_certainly_browser_timestamp); | 
|  | } | 
|  | } | 
|  | aborted_provisional_loads_.clear(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<PageLoadTracker> | 
|  | MetricsWebContentsObserver::NotifyAbortedProvisionalLoadsNewNavigation( | 
|  | content::NavigationHandle* new_navigation, | 
|  | UserInitiatedInfo user_initiated_info) { | 
|  | // Prerendering navigations do not abort provisional loads in the active page. | 
|  | if (new_navigation->IsInPrerenderedMainFrame()) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | // If there are multiple aborted loads that can be attributed to this one, | 
|  | // just count the latest one for simplicity. Other loads will fall into the | 
|  | // OTHER bucket, though there shouldn't be very many. | 
|  | if (aborted_provisional_loads_.empty()) { | 
|  | return nullptr; | 
|  | } | 
|  | if (aborted_provisional_loads_.size() > 1) { | 
|  | RecordInternalError(ERR_NAVIGATION_SIGNALS_MULIPLE_ABORTED_LOADS); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<PageLoadTracker> last_aborted_load = | 
|  | std::move(aborted_provisional_loads_.back()); | 
|  | aborted_provisional_loads_.pop_back(); | 
|  |  | 
|  | base::TimeTicks timestamp = new_navigation->NavigationStart(); | 
|  | if (last_aborted_load->IsLikelyProvisionalAbort(timestamp)) { | 
|  | last_aborted_load->UpdatePageEnd( | 
|  | EndReasonForPageTransition(new_navigation->GetPageTransition()), | 
|  | user_initiated_info, timestamp, false); | 
|  | } | 
|  |  | 
|  | aborted_provisional_loads_.clear(); | 
|  | return last_aborted_load; | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnTimingUpdated( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | mojom::PageLoadTimingPtr timing, | 
|  | mojom::FrameMetadataPtr metadata, | 
|  | const std::vector<blink::UseCounterFeature>& new_features, | 
|  | const std::vector<mojom::ResourceDataUpdatePtr>& resources, | 
|  | mojom::FrameRenderDataUpdatePtr render_data, | 
|  | mojom::CpuTimingPtr cpu_timing, | 
|  | mojom::InputTimingPtr input_timing_delta, | 
|  | const std::optional<blink::SubresourceLoadMetrics>& | 
|  | subresource_load_metrics, | 
|  | mojom::SoftNavigationMetricsPtr soft_navigation_metrics) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTrackerIfValid(render_frame_host)) { | 
|  | tracker->UpdateMetrics( | 
|  | render_frame_host, std::move(timing), std::move(metadata), | 
|  | std::move(new_features), resources, std::move(render_data), | 
|  | std::move(cpu_timing), std::move(input_timing_delta), | 
|  | subresource_load_metrics, std::move(soft_navigation_metrics)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnCustomUserTimingUpdated( | 
|  | content::RenderFrameHost* rfh, | 
|  | mojom::CustomUserTimingMarkPtr custom_timing) { | 
|  | // Buffer timing data before seinding to the tracker as the tracker may not | 
|  | // exist in some cases, in that case the buffered timings are sent next time. | 
|  | page_load_custom_timings_.push_back(std::move(custom_timing)); | 
|  | if (PageLoadTracker* tracker = GetPageLoadTrackerIfValid(rfh)) { | 
|  | tracker->AddCustomUserTimings(std::move(page_load_custom_timings_)); | 
|  | } | 
|  | } | 
|  |  | 
|  | bool MetricsWebContentsObserver::DoesTimingUpdateHaveError( | 
|  | PageLoadTracker* tracker) { | 
|  | // TODO(crbug.com/40679416): Update page load metrics IPC validation to ues | 
|  | // mojo::ReportBadMessage. | 
|  | if (!tracker) { | 
|  | RecordInternalError(ERR_IPC_WITH_NO_RELEVANT_LOAD); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (!ShouldTrackScheme(tracker->GetUrl().scheme())) { | 
|  | RecordInternalError(ERR_IPC_FROM_BAD_URL_SCHEME); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::UpdateTiming( | 
|  | mojom::PageLoadTimingPtr timing, | 
|  | mojom::FrameMetadataPtr metadata, | 
|  | const std::vector<blink::UseCounterFeature>& new_features, | 
|  | std::vector<mojom::ResourceDataUpdatePtr> resources, | 
|  | mojom::FrameRenderDataUpdatePtr render_data, | 
|  | mojom::CpuTimingPtr cpu_timing, | 
|  | mojom::InputTimingPtr input_timing_delta, | 
|  | const std::optional<blink::SubresourceLoadMetrics>& | 
|  | subresource_load_metrics, | 
|  | mojom::SoftNavigationMetricsPtr soft_navigation_metrics) { | 
|  | content::RenderFrameHost* render_frame_host = | 
|  | page_load_metrics_receivers_.GetCurrentTargetFrame(); | 
|  | OnTimingUpdated(render_frame_host, std::move(timing), std::move(metadata), | 
|  | new_features, resources, std::move(render_data), | 
|  | std::move(cpu_timing), std::move(input_timing_delta), | 
|  | subresource_load_metrics, std::move(soft_navigation_metrics)); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::AddCustomUserTiming( | 
|  | mojom::CustomUserTimingMarkPtr custom_timing) { | 
|  | content::RenderFrameHost* render_frame_host = | 
|  | page_load_metrics_receivers_.GetCurrentTargetFrame(); | 
|  | OnCustomUserTimingUpdated(render_frame_host, std::move(custom_timing)); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::SetUpSharedMemoryForDroppedFrames( | 
|  | base::ReadOnlySharedMemoryRegion dropped_frames_memory) { | 
|  | content::RenderFrameHost* render_frame_host = | 
|  | page_load_metrics_receivers_.GetCurrentTargetFrame(); | 
|  | const bool is_outermost_main_frame = | 
|  | render_frame_host->GetParentOrOuterDocument() == nullptr; | 
|  | if (!is_outermost_main_frame) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) { | 
|  | tracker->metrics_update_dispatcher()->SetUpSharedMemoryForDroppedFrames( | 
|  | render_frame_host, std::move(dropped_frames_memory)); | 
|  | } else { | 
|  | ukm_dropped_frames_data_.emplace(render_frame_host, | 
|  | std::move(dropped_frames_memory)); | 
|  | } | 
|  | } | 
|  |  | 
|  | bool MetricsWebContentsObserver::ShouldTrackMainFrameNavigation( | 
|  | content::NavigationHandle* navigation_handle) const { | 
|  | CHECK(navigation_handle->IsInMainFrame()); | 
|  | CHECK(!navigation_handle->HasCommitted() || | 
|  | !navigation_handle->IsSameDocument()); | 
|  | // The navigation served from the back-forward cache will use the previously | 
|  | // created tracker for the document. | 
|  | if (navigation_handle->IsServedFromBackForwardCache()) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // For a prerendering activation navigation, we will use a tracker in | 
|  | // `inactive_pages_` created in the initial prerendering navigation. | 
|  | if (navigation_handle->IsPrerenderedPageActivation()) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (navigation_handle->HasCommitted()) { | 
|  | // Ignore Chrome error pages (e.g. No Internet connection). | 
|  | if (navigation_handle->IsErrorPage()) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Ignore network error pages (e.g. 4xx, 5xx). | 
|  | int http_status_code = GetHttpStatusCode(navigation_handle); | 
|  | if (http_status_code > 0 && | 
|  | (http_status_code < 200 || http_status_code >= 400)) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | const GURL& url = navigation_handle->GetURL(); | 
|  | if (embedder_interface_->IsNewTabPageUrl(url) || | 
|  | (embedder_interface_->HasWebUIConfig(url) && | 
|  | !embedder_interface_->IsInternalWebUI(url)) || | 
|  | embedder_interface_->IsNonTabWebUI(url)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return ShouldTrackSchemeForNonWebUI(url.scheme()); | 
|  | } | 
|  |  | 
|  | bool MetricsWebContentsObserver::ShouldTrackScheme( | 
|  | std::string_view scheme) const { | 
|  | // Allow any scheme if we are tracking WebUIs. | 
|  | if (main_frame_is_webui_) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return ShouldTrackSchemeForNonWebUI(scheme); | 
|  | } | 
|  |  | 
|  | bool MetricsWebContentsObserver::ShouldTrackSchemeForNonWebUI( | 
|  | std::string_view scheme) const { | 
|  | return scheme == url::kHttpsScheme || scheme == url::kHttpScheme || | 
|  | scheme == url::kDataScheme || scheme == url::kFileScheme || | 
|  | embedder_interface_->ShouldObserveScheme(scheme); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnBrowserFeatureUsage( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | const std::vector<blink::UseCounterFeature>& new_features) { | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(render_frame_host)) { | 
|  | tracker->metrics_update_dispatcher()->UpdateFeatures(render_frame_host, | 
|  | new_features); | 
|  | } else { | 
|  | RecordInternalError(ERR_BROWSER_USAGE_WITH_NO_RELEVANT_LOAD); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::AddLifecycleObserver( | 
|  | MetricsLifecycleObserver* observer) { | 
|  | if (!lifecycle_observers_.HasObserver(observer)) { | 
|  | lifecycle_observers_.AddObserver(observer); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::RemoveLifecycleObserver( | 
|  | MetricsLifecycleObserver* observer) { | 
|  | lifecycle_observers_.RemoveObserver(observer); | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnPrefetchLikely() { | 
|  | // Prefetching can be triggered by speculation rules (by SpeculationHostImpl:: | 
|  | // UpdateSpeculationCandidates()) or by NavigationPredictor, both of which | 
|  | // work only on behalf of a primary page. | 
|  | if (primary_page_) { | 
|  | primary_page_->OnPrefetchLikely(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnV8MemoryChanged( | 
|  | const std::vector<MemoryUpdate>& memory_updates) { | 
|  | std::map<PageLoadTracker*, std::vector<MemoryUpdate>> per_tracker_updates; | 
|  | for (const MemoryUpdate& update : memory_updates) { | 
|  | content::RenderFrameHost* rfh = | 
|  | content::RenderFrameHost::FromID(update.routing_id); | 
|  | if (!rfh) { | 
|  | continue; | 
|  | } | 
|  | PageLoadTracker* tracker = GetPageLoadTracker(rfh); | 
|  | if (tracker) { | 
|  | per_tracker_updates[tracker].push_back(update); | 
|  | } else { | 
|  | // If the load hasn't committed yet, then memory updates can't be sent | 
|  | // at this time, but will still need to be sent later. Queue the updates | 
|  | // in case `tracker` is null due to the navigation having not yet | 
|  | // completed, in which case the queued updates will be sent when | 
|  | // HandleCommittedNavigationForTrackedLoad is called.  Otherwise, they | 
|  | // will be ignored and cleared when `rfh` is deleted. | 
|  | queued_memory_updates_.push_back(update); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (const auto& map_pair : per_tracker_updates) { | 
|  | map_pair.first->OnV8MemoryChanged(map_pair.second); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnSharedStorageWorkletHostCreated( | 
|  | content::RenderFrameHost* rfh) { | 
|  | if (!rfh) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(rfh)) { | 
|  | tracker->OnSharedStorageWorkletHostCreated(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnSharedStorageSelectURLCalled( | 
|  | content::RenderFrameHost* main_rfh) { | 
|  | if (!main_rfh) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(main_rfh)) { | 
|  | tracker->OnSharedStorageSelectURLCalled(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MetricsWebContentsObserver::OnAdAuctionComplete( | 
|  | content::RenderFrameHost* rfh, | 
|  | bool is_server_auction, | 
|  | bool is_on_device_auction, | 
|  | content::AuctionResult result) { | 
|  | if (!rfh) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(rfh)) { | 
|  | tracker->OnAdAuctionComplete(is_server_auction, is_on_device_auction, | 
|  | result); | 
|  | } | 
|  | } | 
|  |  | 
|  | base::TimeTicks MetricsWebContentsObserver::GetCreated() { | 
|  | return created_; | 
|  | } | 
|  |  | 
|  | // This contains some bugs. RenderFrameHost::IsActive is not relevant to | 
|  | // determine what members we have to search. | 
|  | // | 
|  | // There are some known wrong cases: | 
|  | // | 
|  | // 1. rfh->GetLifecycleState() == kReadyToBeDeleted && rfh is in active_pages_. | 
|  | //    In this case, this method returns null. This case can occur, e.g. | 
|  | //    navigation on a FF root node. | 
|  | // 2. rfh->GetLifecycleState() == kActive && rfh is already deleted via | 
|  | //    RenderFrameDeleted. | 
|  | //    In this case, this method returns primary_page's PageLeadTracker. This | 
|  | //    case can occur if the caller is FrameDeleted and, e.g. deletion of a FF | 
|  | //    root node. | 
|  | // | 
|  | // This is mitigated by using GetPageLoadTracker. | 
|  | // | 
|  | // TODO(crbug.com/40216775): Use GetPageLoadTracker always. | 
|  | PageLoadTracker* MetricsWebContentsObserver::GetPageLoadTrackerLegacy( | 
|  | content::RenderFrameHost* rfh) { | 
|  | if (!rfh) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | if (rfh->GetMainFrame()->IsActive()) { | 
|  | auto it = active_pages_.find(rfh->GetMainFrame()); | 
|  | if (it != active_pages_.end()) { | 
|  | return it->second.get(); | 
|  | } | 
|  | return primary_page_.get(); | 
|  | } | 
|  |  | 
|  | auto it = inactive_pages_.find(rfh->GetMainFrame()); | 
|  | if (it != inactive_pages_.end()) { | 
|  | return it->second.get(); | 
|  | } | 
|  |  | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | PageLoadTracker* MetricsWebContentsObserver::GetPageLoadTracker( | 
|  | content::RenderFrameHost* rfh) { | 
|  | if (!rfh) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | if (rfh->GetPage().IsPrimary()) { | 
|  | return primary_page_.get(); | 
|  | } | 
|  |  | 
|  | { | 
|  | auto it = active_pages_.find(rfh->GetMainFrame()); | 
|  | if (it != active_pages_.end()) { | 
|  | return it->second.get(); | 
|  | } | 
|  | } | 
|  |  | 
|  | { | 
|  | auto it = inactive_pages_.find(rfh->GetMainFrame()); | 
|  | if (it != inactive_pages_.end()) { | 
|  | return it->second.get(); | 
|  | } | 
|  | } | 
|  |  | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | PageLoadTracker* MetricsWebContentsObserver::GetPageLoadTrackerIfValid( | 
|  | content::RenderFrameHost* render_frame_host) { | 
|  | // Replacing this call by GetPageLoadTracker breaks some tests. | 
|  | // | 
|  | // Note that if a PLMO only observes events at outermost page, misusing | 
|  | // primary page's PageLoadTracker for OnTimingUpdated is safe because | 
|  | // PageLoadTracker::UpdateMetrics forwards events unconditionally and | 
|  | // unmodified, and outermost page's MetricsUpdateDispatcher manages all | 
|  | // subframe's timing update. | 
|  | PageLoadTracker* tracker = GetPageLoadTrackerLegacy(render_frame_host); | 
|  | // We may receive notifications from frames that have been navigated away | 
|  | // from. In that case the PageLoadTracker is already destroyed in | 
|  | // DidFinishNavigation (unless it's stored in bfcache). We simply ignore them. | 
|  | if (!tracker && !render_frame_host->GetMainFrame()->IsActive()) { | 
|  | RecordInternalError(ERR_IPC_FROM_WRONG_FRAME); | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | const bool is_main_frame = (render_frame_host->GetParent() == nullptr); | 
|  | if (is_main_frame) { | 
|  | if (DoesTimingUpdateHaveError(tracker)) { | 
|  | return nullptr; | 
|  | } | 
|  | } else if (!tracker) { | 
|  | RecordInternalError(ERR_SUBFRAME_IPC_WITH_NO_RELEVANT_LOAD); | 
|  | } | 
|  |  | 
|  | return tracker; | 
|  | } | 
|  |  | 
|  | PageLoadTracker* MetricsWebContentsObserver::GetAncestralAlivePageLoadTracker( | 
|  | content::RenderFrameHost* rfh) { | 
|  | content::RenderFrameHost* ancestor = rfh; | 
|  | while (ancestor) { | 
|  | ancestor = ancestor->GetMainFrame(); | 
|  |  | 
|  | if (PageLoadTracker* tracker = GetPageLoadTracker(rfh)) { | 
|  | return tracker; | 
|  | } | 
|  |  | 
|  | ancestor = ancestor->GetParentOrOuterDocument(); | 
|  | } | 
|  |  | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | PageLoadMetricsMemoryTracker* MetricsWebContentsObserver::GetMemoryTracker() | 
|  | const { | 
|  | return embedder_interface_->GetMemoryTrackerForBrowserContext( | 
|  | web_contents()->GetBrowserContext()); | 
|  | } | 
|  |  | 
|  | WEB_CONTENTS_USER_DATA_KEY_IMPL(MetricsWebContentsObserver); | 
|  |  | 
|  | }  // namespace page_load_metrics |