| // Copyright 2016 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/page_load_tracker.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <ostream> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/feature_list.h" |
| #include "base/memory/read_only_shared_memory_region.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/default_tick_clock.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/page_load_metrics/browser/features.h" |
| #include "components/page_load_metrics/browser/observers/assert_page_load_metrics_observer.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_embedder_interface.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_forward_observer.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_memory_tracker.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_observer.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_util.h" |
| #include "components/page_load_metrics/common/page_load_timing.h" |
| #include "content/public/browser/navigation_details.h" |
| #include "content/public/browser/navigation_discard_reason.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/web_contents.h" |
| #include "content/public/browser/web_contents_delegate.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "page_load_metrics_observer_delegate.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "third_party/blink/public/mojom/input/input_event.mojom-shared.h" |
| |
| namespace page_load_metrics { |
| |
| namespace internal { |
| |
| const char kErrorEvents[] = "PageLoad.Internal.ErrorCode"; |
| const char kPageLoadPrerender2Event[] = "PageLoad.Internal.Prerender2.Event"; |
| |
| } // namespace internal |
| |
| void RecordInternalError(InternalErrorLoadEvent event) { |
| base::UmaHistogramEnumeration(internal::kErrorEvents, event, ERR_LAST_ENTRY); |
| } |
| |
| // TODO(csharrison): Add a case for client side redirects, which is what JS |
| // initiated window.location / window.history navigations get set to. |
| PageEndReason EndReasonForPageTransition(ui::PageTransition transition) { |
| if (transition & ui::PAGE_TRANSITION_CLIENT_REDIRECT) { |
| return END_CLIENT_REDIRECT; |
| } |
| // Check for forward/back navigations first since there are forward/back |
| // navigations that haved PAGE_TRANSITION_RELOAD but are not user reloads |
| // (pull-to-refresh or preview opt-out). |
| if (transition & ui::PAGE_TRANSITION_FORWARD_BACK) { |
| return END_FORWARD_BACK; |
| } |
| if (ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_RELOAD)) { |
| return END_RELOAD; |
| } |
| if (ui::PageTransitionIsNewNavigation(transition)) { |
| return END_NEW_NAVIGATION; |
| } |
| NOTREACHED() |
| << "EndReasonForPageTransition received unexpected ui::PageTransition: " |
| << transition; |
| } |
| |
| bool IsNavigationUserInitiated(content::NavigationHandle* handle) { |
| // TODO(crbug.com/41257523): Browser initiated navigations should have |
| // HasUserGesture() set to true. In the meantime, we consider all |
| // browser-initiated navigations to be user initiated. |
| // |
| // TODO(crbug.com/40480474): Some browser-initiated navigations incorrectly |
| // report that they are renderer-initiated. We will currently report that |
| // these navigations are not user initiated, when in fact they are user |
| // initiated. |
| return handle->HasUserGesture() || !handle->IsRendererInitiated(); |
| } |
| |
| namespace { |
| |
| void DispatchEventsAfterBackForwardCacheRestore( |
| PageLoadMetricsObserverInterface* observer, |
| const std::vector<mojo::StructPtr<mojom::BackForwardCacheTiming>>& |
| last_timings, |
| const std::vector<mojo::StructPtr<mojom::BackForwardCacheTiming>>& |
| new_timings) { |
| if (new_timings.size() < last_timings.size()) { |
| mojo::ReportBadMessage(base::StringPrintf( |
| "`new_timings.size()` (%zu) must be equal to or greater than " |
| "`last_timings.size()` (%zu) but is not", |
| new_timings.size(), last_timings.size())); |
| return; |
| } |
| |
| for (size_t i = 0; i < new_timings.size(); i++) { |
| auto first_paint = |
| new_timings[i]->first_paint_after_back_forward_cache_restore; |
| if (!first_paint.is_zero() && |
| (i >= last_timings.size() || |
| last_timings[i] |
| ->first_paint_after_back_forward_cache_restore.is_zero())) { |
| observer->OnFirstPaintAfterBackForwardCacheRestoreInPage(*new_timings[i], |
| i); |
| } |
| |
| auto request_animation_frames = |
| new_timings[i] |
| ->request_animation_frames_after_back_forward_cache_restore; |
| if (request_animation_frames.size() == 3 && |
| (i >= last_timings.size() || |
| last_timings[i] |
| ->request_animation_frames_after_back_forward_cache_restore |
| .empty())) { |
| observer->OnRequestAnimationFramesAfterBackForwardCacheRestoreInPage( |
| *new_timings[i], i); |
| } |
| |
| auto first_input_delay = |
| new_timings[i]->first_input_delay_after_back_forward_cache_restore; |
| if (first_input_delay.has_value() && |
| (i >= last_timings.size() || |
| !last_timings[i] |
| ->first_input_delay_after_back_forward_cache_restore |
| .has_value())) { |
| observer->OnFirstInputAfterBackForwardCacheRestoreInPage(*new_timings[i], |
| i); |
| } |
| } |
| } |
| |
| void DispatchObserverTimingCallbacks(PageLoadMetricsObserverInterface* observer, |
| const mojom::PageLoadTiming& last_timing, |
| const mojom::PageLoadTiming& new_timing) { |
| if (!last_timing.Equals(new_timing)) { |
| observer->OnTimingUpdate(nullptr, new_timing); |
| } |
| if (new_timing.document_timing->dom_content_loaded_event_start && |
| !last_timing.document_timing->dom_content_loaded_event_start) { |
| observer->OnDomContentLoadedEventStart(new_timing); |
| } |
| if (new_timing.document_timing->load_event_start && |
| !last_timing.document_timing->load_event_start) { |
| observer->OnLoadEventStart(new_timing); |
| } |
| if (new_timing.interactive_timing->first_input_delay && |
| !last_timing.interactive_timing->first_input_delay) { |
| observer->OnFirstInputInPage(new_timing); |
| } |
| if (new_timing.paint_timing->first_paint && |
| !last_timing.paint_timing->first_paint) { |
| observer->OnFirstPaintInPage(new_timing); |
| } |
| DispatchEventsAfterBackForwardCacheRestore( |
| observer, last_timing.back_forward_cache_timings, |
| new_timing.back_forward_cache_timings); |
| if (new_timing.paint_timing->first_image_paint && |
| !last_timing.paint_timing->first_image_paint) { |
| observer->OnFirstImagePaintInPage(new_timing); |
| } |
| if (new_timing.paint_timing->first_contentful_paint && |
| !last_timing.paint_timing->first_contentful_paint) { |
| observer->OnFirstContentfulPaintInPage(new_timing); |
| } |
| if (new_timing.paint_timing->first_meaningful_paint && |
| !last_timing.paint_timing->first_meaningful_paint) { |
| observer->OnFirstMeaningfulPaintInMainFrameDocument(new_timing); |
| } |
| if (new_timing.parse_timing->parse_start && |
| !last_timing.parse_timing->parse_start) { |
| observer->OnParseStart(new_timing); |
| } |
| if (new_timing.parse_timing->parse_stop && |
| !last_timing.parse_timing->parse_stop) { |
| observer->OnParseStop(new_timing); |
| } |
| if (new_timing.domain_lookup_timing->domain_lookup_start && |
| !last_timing.domain_lookup_timing->domain_lookup_start) { |
| observer->OnDomainLookupStart(new_timing); |
| } |
| if (new_timing.domain_lookup_timing->domain_lookup_end && |
| !last_timing.domain_lookup_timing->domain_lookup_end) { |
| observer->OnDomainLookupEnd(new_timing); |
| } |
| if (new_timing.connect_start && !last_timing.connect_start) { |
| observer->OnConnectStart(new_timing); |
| } |
| if (new_timing.connect_end && !last_timing.connect_end) { |
| observer->OnConnectEnd(new_timing); |
| } |
| if (new_timing.user_timing_mark_fully_loaded != |
| last_timing.user_timing_mark_fully_loaded) { |
| observer->OnUserTimingMarkFullyLoaded(new_timing); |
| } |
| if (new_timing.user_timing_mark_fully_visible != |
| last_timing.user_timing_mark_fully_visible) { |
| observer->OnUserTimingMarkFullyVisible(new_timing); |
| } |
| if (new_timing.user_timing_mark_interactive != |
| last_timing.user_timing_mark_interactive) { |
| observer->OnUserTimingMarkInteractive(new_timing); |
| } |
| } |
| |
| internal::PageLoadTrackerPageType CalculatePageType( |
| content::NavigationHandle* navigation_handle) { |
| if (navigation_handle->IsInPrerenderedMainFrame()) { |
| return internal::PageLoadTrackerPageType::kPrerenderPage; |
| } else if (navigation_handle->GetNavigatingFrameType() == |
| content::FrameType::kFencedFrameRoot) { |
| return internal::PageLoadTrackerPageType::kFencedFramesPage; |
| } |
| return navigation_handle->GetWebContents()->IsInPreviewMode() |
| ? internal::PageLoadTrackerPageType::kPreviewPrimaryPage |
| : internal::PageLoadTrackerPageType::kPrimaryPage; |
| } |
| |
| bool CalculateIsOriginVisit(bool is_first_navigation, |
| ui::PageTransition transition) { |
| if (is_first_navigation) { |
| return true; |
| } |
| if (ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_LINK) || |
| ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_RELOAD)) { |
| return false; |
| } |
| return true; |
| } |
| |
| void RegisterObservers(PageLoadTracker* tracker, |
| PageLoadMetricsEmbedderInterface* embedder, |
| content::NavigationHandle* navigation_handle) { |
| #if DCHECK_IS_ON() |
| // Link Preview doesn't emit activation event yet and assertion of event |
| // orders fail. |
| // |
| // TODO(b:302999778): Reenable it. |
| if (!tracker->GetWebContents()->IsInPreviewMode()) { |
| tracker->AddObserver(std::make_unique<AssertPageLoadMetricsObserver>()); |
| } |
| #endif |
| embedder->RegisterObservers(tracker, navigation_handle); |
| } |
| |
| } // namespace |
| |
| PageLoadTracker::PageLoadTracker( |
| bool in_foreground, |
| PageLoadMetricsEmbedderInterface* embedder_interface, |
| const GURL& currently_committed_url, |
| bool is_first_navigation_in_web_contents, |
| content::NavigationHandle* navigation_handle, |
| UserInitiatedInfo user_initiated_info, |
| ukm::SourceId source_id, |
| base::WeakPtr<PageLoadTracker> parent_tracker) |
| : did_stop_tracking_(false), |
| navigation_id_(navigation_handle->GetNavigationId()), |
| navigation_start_(navigation_handle->NavigationStart()), |
| url_(navigation_handle->GetURL()), |
| start_url_(navigation_handle->GetURL()), |
| visibility_tracker_(base::DefaultTickClock::GetInstance(), in_foreground), |
| did_commit_(false), |
| page_end_reason_(END_NONE), |
| page_end_user_initiated_info_(UserInitiatedInfo::NotUserInitiated()), |
| started_in_foreground_(in_foreground), |
| last_dispatched_merged_page_timing_(CreatePageLoadTiming()), |
| user_initiated_info_(user_initiated_info), |
| embedder_interface_(embedder_interface), |
| metrics_update_dispatcher_(this, navigation_handle, embedder_interface), |
| source_id_(source_id), |
| web_contents_(navigation_handle->GetWebContents()), |
| is_first_navigation_in_web_contents_(is_first_navigation_in_web_contents), |
| is_origin_visit_( |
| CalculateIsOriginVisit(is_first_navigation_in_web_contents, |
| navigation_handle->GetPageTransition())), |
| soft_navigation_metrics_(CreateSoftNavigationMetrics()), |
| page_type_(CalculatePageType(navigation_handle)), |
| parent_tracker_(std::move(parent_tracker)) { |
| DCHECK(!navigation_handle->HasCommitted()); |
| RegisterObservers(this, embedder_interface, navigation_handle); |
| switch (page_type_) { |
| case internal::PageLoadTrackerPageType::kPrimaryPage: |
| CHECK_NE(ukm::kInvalidSourceId, source_id_); |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnStart", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| const GURL& currently_committed_url, |
| bool started_in_foreground, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnStart(navigation_handle, |
| currently_committed_url, |
| started_in_foreground); |
| }, |
| navigation_handle, currently_committed_url, |
| started_in_foreground_), |
| /*permit_forwarding=*/false); |
| break; |
| case internal::PageLoadTrackerPageType::kPrerenderPage: |
| CHECK(!started_in_foreground_); |
| CHECK_EQ(ukm::kInvalidSourceId, source_id_); |
| prerendering_state_ = PrerenderingState::kInPrerendering; |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnPrerenderStart", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| const GURL& currently_committed_url, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnPrerenderStart(navigation_handle, |
| currently_committed_url); |
| }, |
| navigation_handle, currently_committed_url), |
| /*permit_forwarding=*/false); |
| base::UmaHistogramEnumeration( |
| internal::kPageLoadPrerender2Event, |
| internal::PageLoadPrerenderEvent::kNavigationInPrerenderedMainFrame); |
| break; |
| case internal::PageLoadTrackerPageType::kFencedFramesPage: |
| CHECK_NE(ukm::kInvalidSourceId, source_id_); |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnFencedFramesStart", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| const GURL& currently_committed_url, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnFencedFramesStart(navigation_handle, |
| currently_committed_url); |
| }, |
| navigation_handle, currently_committed_url), |
| /*permit_forwarding=*/true); |
| break; |
| case internal::PageLoadTrackerPageType::kPreviewPrimaryPage: |
| CHECK_NE(ukm::kInvalidSourceId, source_id_); |
| prerendering_state_ = PrerenderingState::kInPreview; |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnPreviewStart", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| const GURL& currently_committed_url, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnPreviewStart(navigation_handle, |
| currently_committed_url); |
| }, |
| navigation_handle, currently_committed_url), |
| /*permit_forwarding=*/false); |
| break; |
| } |
| } |
| |
| PageLoadTracker::~PageLoadTracker() { |
| if (did_stop_tracking_) { |
| return; |
| } |
| |
| metrics_update_dispatcher_.ShutDown(); |
| |
| if (page_end_time_.is_null()) { |
| // page_end_time_ can be unset in some cases, such as when a navigation is |
| // aborted by a navigation that started before it. In these cases, set the |
| // end time to the current time. |
| RecordInternalError(ERR_NO_PAGE_LOAD_END_TIME); |
| NotifyPageEnd(END_OTHER, UserInitiatedInfo::NotUserInitiated(), |
| base::TimeTicks::Now(), true); |
| } |
| |
| if (!did_commit_) { |
| if (!failed_provisional_load_info_) { |
| RecordInternalError(ERR_NO_COMMIT_OR_FAILED_PROVISIONAL_LOAD); |
| } |
| } else if (page_load_metrics::IsEmpty(metrics_update_dispatcher_.timing())) { |
| RecordInternalError(ERR_NO_IPCS_RECEIVED); |
| } |
| |
| for (const auto& observer : observers_) { |
| if (failed_provisional_load_info_) { |
| observer->OnFailedProvisionalLoad(*failed_provisional_load_info_); |
| } else { |
| DCHECK(did_commit_); |
| observer->OnComplete(metrics_update_dispatcher_.timing()); |
| } |
| } |
| } |
| |
| void PageLoadTracker::PageHidden() { |
| // Only log the first time we background in a given page load. |
| if (!first_background_time_.has_value() || |
| (!back_forward_cache_restores_.empty() && |
| !back_forward_cache_restores_.back() |
| .first_background_time.has_value())) { |
| // The possible visibility state transitions and events are: |
| // |
| // A. navigation_start (fg) -> first background (bg) |
| // -> first foreground (fg) |
| // B. navigation_start (bg) -> first foreground (fg) |
| // -> first background (bg) |
| // C. navigation_start (bg) -> activation_start (fg) |
| // -> first background (bg) -> first foreground (fg) |
| // |
| // where fg = foreground, bg = background. A and B are non prerendered and C |
| // is prerendered. |
| // |
| // PageShown and PgaeHidden are not called for navigation_start and |
| // actiivation_start; called when the visibility is changed after |
| // navigation_start (resp. activation_start) for non prerendered (resp. |
| // prerendered) pages. |
| // |
| // Here we check that the first background follows some event in foreground. |
| if (!first_background_time_.has_value()) { |
| if (prerendering_state_ == PrerenderingState::kNoPrerendering || |
| prerendering_state_ == PrerenderingState::kInPreview) { |
| DCHECK_EQ(!started_in_foreground_, first_foreground_time_.has_value()); |
| } else { |
| DCHECK(!first_foreground_time_.has_value()); |
| } |
| } |
| |
| base::TimeTicks background_time = base::TimeTicks::Now(); |
| ClampBrowserTimestampIfInterProcessTimeTickSkew(&background_time); |
| DCHECK_GE(background_time, navigation_start_); |
| |
| if (!first_background_time_.has_value()) { |
| first_background_time_ = background_time; |
| } |
| |
| if (!back_forward_cache_restores_.empty() && |
| !back_forward_cache_restores_.back() |
| .first_background_time.has_value()) { |
| back_forward_cache_restores_.back().first_background_time = |
| background_time - |
| back_forward_cache_restores_.back().navigation_start_time; |
| } |
| } |
| visibility_tracker_.OnHidden(); |
| InvokeAndPruneObservers("PageLoadMetricsObserver::OnHidden", |
| base::BindRepeating( |
| [](const mojom::PageLoadTiming* timing, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnHidden(*timing); |
| }, |
| &metrics_update_dispatcher_.timing()), |
| /*permit_forwarding=*/false); |
| } |
| |
| void PageLoadTracker::PageShown() { |
| // Only log the first time we foreground in a given page load. |
| if (!first_foreground_time_.has_value()) { |
| // See comment about visibility state transitions in PageHidden. |
| // |
| // Here we check that the first foreground follows some event in background. |
| if (prerendering_state_ == PrerenderingState::kNoPrerendering || |
| prerendering_state_ == PrerenderingState::kInPreview) { |
| DCHECK_EQ(started_in_foreground_, first_background_time_.has_value()); |
| } else { |
| DCHECK(first_background_time_.has_value()); |
| } |
| |
| base::TimeTicks foreground_time = base::TimeTicks::Now(); |
| ClampBrowserTimestampIfInterProcessTimeTickSkew(&foreground_time); |
| DCHECK_GE(foreground_time, navigation_start_); |
| first_foreground_time_ = foreground_time; |
| } |
| |
| visibility_tracker_.OnShown(); |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnShown", |
| base::BindRepeating([](PageLoadMetricsObserverInterface* observer) { |
| return observer->OnShown(); |
| }), |
| /*permit_forwarding=*/false); |
| } |
| |
| void PageLoadTracker::RenderFrameDeleted(content::RenderFrameHost* rfh) { |
| if (parent_tracker_) { |
| // Notify the parent of a deletion of RenderFrameHost of a subframe. |
| parent_tracker_->RenderFrameDeleted(rfh); |
| } |
| |
| for (const auto& observer : observers_) { |
| observer->OnRenderFrameDeleted(rfh); |
| } |
| } |
| |
| void PageLoadTracker::FrameTreeNodeDeleted( |
| content::FrameTreeNodeId frame_tree_node_id) { |
| if (parent_tracker_) { |
| // Notify the parent of a deletion of FrameTreeNode of a subframe. |
| // |
| // Note that deletion of main frames are forwarded by |
| // MetrcisWebContentsObserver. |
| parent_tracker_->FrameTreeNodeDeleted(frame_tree_node_id); |
| } |
| |
| metrics_update_dispatcher_.OnSubFrameDeleted(frame_tree_node_id); |
| largest_contentful_paint_handler_.OnSubFrameDeleted(frame_tree_node_id); |
| for (const auto& observer : observers_) { |
| observer->OnSubFrameDeleted(frame_tree_node_id); |
| } |
| } |
| |
| void PageLoadTracker::WillProcessNavigationResponse( |
| content::NavigationHandle* navigation_handle) { |
| DCHECK(!navigation_request_id_.has_value()); |
| navigation_request_id_ = navigation_handle->GetGlobalRequestID(); |
| } |
| |
| void PageLoadTracker::Commit(content::NavigationHandle* navigation_handle) { |
| // We don't deliver OnCommit() for activation. Prerendered pages will see |
| // DidActivatePrerenderedPage() instead. |
| // Event records below are also not needed as we did them for the initial |
| // navigation on starting prerendering. |
| DCHECK(!navigation_handle->IsPrerenderedPageActivation()); |
| |
| if (parent_tracker_) { |
| // Notify the parent of the inner main frame navigation as a sub-frame |
| // navigation. |
| parent_tracker_->DidFinishSubFrameNavigation(navigation_handle); |
| } else if (navigation_handle->IsPrerenderedPageActivation()) { |
| // We don't deliver OnCommit() for activation. Prerendered pages will see |
| // DidActivatePrerenderedPage() instead. |
| // Event records below are also not needed as we did them for the initial |
| // navigation on starting prerendering. |
| NOTREACHED(); |
| } |
| |
| did_commit_ = true; |
| url_ = navigation_handle->GetURL(); |
| // Some transitions (like CLIENT_REDIRECT) are only known at commit time. |
| user_initiated_info_.user_gesture = navigation_handle->HasUserGesture(); |
| |
| if (navigation_handle->IsInMainFrame()) { |
| largest_contentful_paint_handler_.RecordMainFrameTreeNodeId( |
| navigation_handle->GetFrameTreeNodeId()); |
| experimental_largest_contentful_paint_handler_.RecordMainFrameTreeNodeId( |
| navigation_handle->GetFrameTreeNodeId()); |
| } |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::ShouldObserveMimeType", |
| base::BindRepeating( |
| [](const std::string& mime_type, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->ShouldObserveMimeType(mime_type); |
| }, |
| // Query with the outermost page's MIME type so that we can ask each |
| // observer with information for the page they are interested in. |
| navigation_handle->GetRenderFrameHost() |
| ->GetOutermostMainFrameOrEmbedder() |
| ->GetPage() |
| .GetContentsMimeType()), |
| /*permit_forwarding=*/false); |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::ShouldObserveScheme", |
| base::BindRepeating( |
| [](const GURL& url, PageLoadMetricsObserverInterface* observer) { |
| return observer->ShouldObserveScheme(url); |
| }, |
| navigation_handle->GetURL()), |
| /*permit_forwarding=*/false); |
| InvokeAndPruneObservers("PageLoadMetricsObserver::OnCommit", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnCommit(navigation_handle); |
| }, |
| navigation_handle), |
| /*permit_forwarding=*/false); |
| } |
| |
| void PageLoadTracker::DidActivatePrerenderedPage( |
| content::NavigationHandle* navigation_handle) { |
| DCHECK_EQ(prerendering_state_, PrerenderingState::kInPrerendering); |
| |
| prerendering_state_ = PrerenderingState::kActivatedNoActivationStart; |
| source_id_ = ukm::ConvertToSourceId(navigation_handle->GetNavigationId(), |
| ukm::SourceIdType::NAVIGATION_ID); |
| |
| switch (GetWebContents()->GetVisibility()) { |
| case content::Visibility::HIDDEN: |
| case content::Visibility::OCCLUDED: |
| visibility_at_activation_ = PageVisibility::kBackground; |
| break; |
| case content::Visibility::VISIBLE: |
| visibility_at_activation_ = PageVisibility::kForeground; |
| break; |
| } |
| |
| for (const auto& observer : observers_) { |
| observer->DidActivatePrerenderedPage(navigation_handle); |
| } |
| |
| base::UmaHistogramEnumeration( |
| internal::kPageLoadPrerender2Event, |
| internal::PageLoadPrerenderEvent::kPrerenderActivationNavigation); |
| } |
| |
| void PageLoadTracker::DidActivatePreviewedPage( |
| base::TimeTicks activation_time) { |
| CHECK_EQ(prerendering_state_, PrerenderingState::kInPreview); |
| prerendering_state_ = PrerenderingState::kNoPrerendering; |
| |
| // We don't keep `activation_time` as `activation_start_` because we measure |
| // preview mode performance as navigation originated rather than activation. |
| |
| for (const auto& observer : observers_) { |
| observer->DidActivatePreviewedPage(activation_time); |
| } |
| } |
| |
| void PageLoadTracker::DidCommitSameDocumentNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (parent_tracker_) { |
| // Notify the parent of the inner main frame navigation as a sub-frame |
| // navigation. |
| parent_tracker_->DidFinishSubFrameNavigation(navigation_handle); |
| } |
| |
| // Update soft navigation URL and UKM source id; |
| // A same-document navigation may not be a soft navigation. But when a soft |
| // navigation updates comes in later, the URL and source id updated here would |
| // correspond to that soft navigation. |
| if (navigation_handle->IsInMainFrame()) { |
| previous_soft_navigation_source_id_ = potential_soft_navigation_source_id_; |
| potential_soft_navigation_source_id_ = |
| ukm::ConvertToSourceId(navigation_handle->GetNavigationId(), |
| ukm::SourceIdObj::Type::NAVIGATION_ID); |
| } |
| |
| for (const auto& observer : observers_) { |
| observer->OnCommitSameDocumentNavigation(navigation_handle); |
| } |
| } |
| |
| void PageLoadTracker::DidInternalNavigationAbort( |
| content::NavigationHandle* navigation_handle) { |
| if (parent_tracker_) { |
| // Notify the parent of the inner main frame navigation as a sub-frame |
| // navigation. |
| parent_tracker_->DidFinishSubFrameNavigation(navigation_handle); |
| } |
| |
| for (const auto& observer : observers_) { |
| observer->OnDidInternalNavigationAbort(navigation_handle); |
| } |
| } |
| |
| void PageLoadTracker::ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // Don't notify the parent as inner main frame's events are converted to |
| // sub-frames events for the parent, but this event is only for the main |
| // frame. |
| |
| for (const auto& observer : observers_) { |
| observer->ReadyToCommitNextNavigation(navigation_handle); |
| } |
| } |
| |
| void PageLoadTracker::DidFinishSubFrameNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (parent_tracker_) { |
| // Notify the parent of inner frame navigations. |
| parent_tracker_->DidFinishSubFrameNavigation(navigation_handle); |
| } |
| metrics_update_dispatcher_.DidFinishSubFrameNavigation(navigation_handle); |
| largest_contentful_paint_handler_.OnDidFinishSubFrameNavigation( |
| navigation_handle, navigation_start_); |
| experimental_largest_contentful_paint_handler_.OnDidFinishSubFrameNavigation( |
| navigation_handle, navigation_start_); |
| for (const auto& observer : observers_) { |
| observer->OnDidFinishSubFrameNavigation(navigation_handle); |
| } |
| } |
| |
| void PageLoadTracker::FailedProvisionalLoad( |
| content::NavigationHandle* navigation_handle, |
| base::TimeTicks failed_load_time) { |
| DCHECK(!failed_provisional_load_info_); |
| if (parent_tracker_) { |
| // Notify the parent of the inner main frame navigation as a sub-frame |
| // navigation. |
| parent_tracker_->DidFinishSubFrameNavigation(navigation_handle); |
| } |
| CHECK(navigation_handle->GetNavigationDiscardReason().has_value()); |
| failed_provisional_load_info_ = std::make_unique<FailedProvisionalLoadInfo>( |
| failed_load_time - navigation_handle->NavigationStart(), |
| navigation_handle->GetNetErrorCode(), |
| navigation_handle->GetNetExtendedErrorCode(), |
| navigation_handle->GetErrorNavigationTrigger(), |
| navigation_handle->GetNavigationDiscardReason().value()); |
| } |
| |
| void PageLoadTracker::DidUpdateNavigationHandleTiming( |
| content::NavigationHandle* navigation_handle) { |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnNavigationHandleTimingUpdated", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnNavigationHandleTimingUpdated(navigation_handle); |
| }, |
| navigation_handle), |
| /*permit_forwarding=*/false); |
| } |
| |
| void PageLoadTracker::Redirect(content::NavigationHandle* navigation_handle) { |
| url_ = navigation_handle->GetURL(); |
| InvokeAndPruneObservers("PageLoadMetricsObserver::Redirect", |
| base::BindRepeating( |
| [](content::NavigationHandle* navigation_handle, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnRedirect(navigation_handle); |
| }, |
| navigation_handle), |
| /*permit_forwarding=*/false); |
| } |
| |
| void PageLoadTracker::OnInputEvent(const blink::WebInputEvent& event) { |
| static const bool do_not_send_continuous_events = |
| !base::FeatureList::IsEnabled( |
| features::kSendContinuousInputEventsToObservers); |
| using Type = blink::mojom::EventType; |
| const bool is_continuous_event = |
| (event.GetType() == Type::kTouchMove || |
| event.GetType() == Type::kGestureScrollUpdate || |
| event.GetType() == Type::kGesturePinchUpdate); |
| if (do_not_send_continuous_events && is_continuous_event) { |
| return; |
| } |
| |
| // TODO(b/328601354): Confirm continuous input events are not required for |
| // page load tracker observers and rename the API to reflect the same. |
| for (const auto& observer : observers_) { |
| observer->OnUserInput(event, metrics_update_dispatcher_.timing()); |
| } |
| } |
| |
| void PageLoadTracker::FlushMetricsOnAppEnterBackground() { |
| metrics_update_dispatcher()->FlushPendingTimingUpdates(); |
| |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::FlushMetricsOnAppEnterBackground", |
| base::BindRepeating( |
| [](const mojom::PageLoadTiming* timing, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->FlushMetricsOnAppEnterBackground(*timing); |
| }, |
| &metrics_update_dispatcher_.timing()), |
| /*permit_forwarding=*/false); |
| } |
| |
| void PageLoadTracker::OnLoadedResource( |
| const ExtraRequestCompleteInfo& extra_request_complete_info) { |
| for (const auto& observer : observers_) { |
| observer->OnLoadedResource(extra_request_complete_info); |
| } |
| } |
| |
| void PageLoadTracker::FrameReceivedUserActivation( |
| content::RenderFrameHost* rfh) { |
| for (const auto& observer : observers_) { |
| observer->FrameReceivedUserActivation(rfh); |
| } |
| } |
| |
| void PageLoadTracker::FrameDisplayStateChanged( |
| content::RenderFrameHost* render_frame_host, |
| bool is_display_none) { |
| for (const auto& observer : observers_) { |
| observer->FrameDisplayStateChanged(render_frame_host, is_display_none); |
| } |
| } |
| |
| void PageLoadTracker::FrameSizeChanged( |
| content::RenderFrameHost* render_frame_host, |
| const gfx::Size& frame_size) { |
| for (const auto& observer : observers_) { |
| observer->FrameSizeChanged(render_frame_host, frame_size); |
| } |
| } |
| |
| void PageLoadTracker::OnCookiesRead( |
| const GURL& url, |
| const GURL& first_party_url, |
| bool blocked_by_policy, |
| bool is_ad_tagged, |
| const net::CookieSettingOverrides& cookie_setting_overrides, |
| bool is_partitioned_access) { |
| for (const auto& observer : observers_) { |
| observer->OnCookiesRead(url, first_party_url, blocked_by_policy, |
| is_ad_tagged, cookie_setting_overrides, |
| is_partitioned_access); |
| } |
| } |
| |
| void PageLoadTracker::OnCookieChange( |
| const GURL& url, |
| const GURL& first_party_url, |
| const net::CanonicalCookie& cookie, |
| bool blocked_by_policy, |
| bool is_ad_tagged, |
| const net::CookieSettingOverrides& cookie_setting_overrides, |
| bool is_partitioned_access) { |
| for (const auto& observer : observers_) { |
| observer->OnCookieChange(url, first_party_url, cookie, blocked_by_policy, |
| is_ad_tagged, cookie_setting_overrides, |
| is_partitioned_access); |
| } |
| } |
| |
| void PageLoadTracker::OnStorageAccessed(const GURL& url, |
| const GURL& first_party_url, |
| bool blocked_by_policy, |
| StorageType access_type) { |
| for (const auto& observer : observers_) { |
| observer->OnStorageAccessed(url, first_party_url, blocked_by_policy, |
| access_type); |
| } |
| } |
| |
| void PageLoadTracker::StopTracking() { |
| did_stop_tracking_ = true; |
| observers_map_.clear(); |
| observers_.clear(); |
| } |
| |
| void PageLoadTracker::AddObserver( |
| std::unique_ptr<PageLoadMetricsObserverInterface> observer) { |
| observer->SetDelegate(this); |
| |
| if (observer->GetObserverName()) { |
| DCHECK(observers_map_.find(observer->GetObserverName()) == |
| observers_map_.end()) |
| << "We expect that observer's class and name is unique in trackers. " |
| "Note that observer's class can be non-unique in test, e.g. " |
| "PageLoadMetricsTestWaiter. In that case, use a unique name in " |
| "the test. See also constructor of PageLoadMetricsTestWaiter."; |
| observers_map_.emplace(observer->GetObserverName(), observer.get()); |
| } |
| |
| observers_.push_back(std::move(observer)); |
| } |
| |
| base::WeakPtr<PageLoadMetricsObserverInterface> PageLoadTracker::FindObserver( |
| const char* name) { |
| auto it = observers_map_.find(name); |
| if (it != observers_map_.end()) { |
| return it->second->GetWeakPtr(); |
| } |
| return nullptr; |
| } |
| |
| void PageLoadTracker::ClampBrowserTimestampIfInterProcessTimeTickSkew( |
| base::TimeTicks* event_time) { |
| DCHECK(event_time != nullptr); |
| // Windows 10 GCE bot non-deterministically failed because TimeTicks::Now() |
| // called in the browser process e.g. commit_time was less than |
| // navigation_start_ that was populated in the renderer process because the |
| // clock was not system-wide monotonic. |
| // Note that navigation_start_ can also be set in the browser process in |
| // some cases and in those cases event_time should never be < |
| // navigation_start_. If it is due to a code error and it gets clamped in this |
| // function, on high resolution systems it should lead to a dcheck failure. |
| |
| // TODO(shivanisha): Currently IsHighResolution is the best way to check |
| // if the clock is system-wide monotonic. However IsHighResolution |
| // does a broader check to see if the clock in use is high resolution |
| // which also implies it is system-wide monotonic (on Windows). |
| if (base::TimeTicks::IsHighResolution()) { |
| DCHECK(event_time->is_null() || *event_time >= navigation_start_); |
| return; |
| } |
| |
| if (!event_time->is_null() && *event_time < navigation_start_) { |
| RecordInternalError(ERR_INTER_PROCESS_TIME_TICK_SKEW); |
| *event_time = navigation_start_; |
| } |
| } |
| |
| bool PageLoadTracker::HasMatchingNavigationRequestID( |
| const content::GlobalRequestID& request_id) const { |
| DCHECK(request_id != content::GlobalRequestID()); |
| return navigation_request_id_.has_value() && |
| navigation_request_id_.value() == request_id; |
| } |
| |
| void PageLoadTracker::NotifyPageEnd(PageEndReason page_end_reason, |
| UserInitiatedInfo user_initiated_info, |
| base::TimeTicks timestamp, |
| bool is_certainly_browser_timestamp) { |
| DCHECK_NE(page_end_reason, END_NONE); |
| // Use UpdatePageEnd to update an already notified PageLoadTracker. |
| if (page_end_reason_ != END_NONE) { |
| return; |
| } |
| |
| UpdatePageEndInternal(page_end_reason, user_initiated_info, timestamp, |
| is_certainly_browser_timestamp); |
| } |
| |
| void PageLoadTracker::UpdatePageEnd(PageEndReason page_end_reason, |
| UserInitiatedInfo user_initiated_info, |
| base::TimeTicks timestamp, |
| bool is_certainly_browser_timestamp) { |
| DCHECK_NE(page_end_reason, END_NONE); |
| DCHECK_NE(page_end_reason, END_OTHER); |
| DCHECK_EQ(page_end_reason_, END_OTHER); |
| DCHECK(!page_end_time_.is_null()); |
| if (page_end_time_.is_null() || page_end_reason_ != END_OTHER) { |
| return; |
| } |
| |
| // For some aborts (e.g. navigations), the initiated timestamp can be earlier |
| // than the timestamp that aborted the load. Taking the minimum gives the |
| // closest user initiated time known. |
| UpdatePageEndInternal(page_end_reason, user_initiated_info, |
| std::min(page_end_time_, timestamp), |
| is_certainly_browser_timestamp); |
| } |
| |
| bool PageLoadTracker::IsLikelyProvisionalAbort( |
| base::TimeTicks abort_cause_time) const { |
| // Note that |abort_cause_time - page_end_time_| can be negative. |
| return page_end_reason_ == END_OTHER && |
| (abort_cause_time - page_end_time_).InMilliseconds() < 100; |
| } |
| |
| void PageLoadTracker::UpdatePageEndInternal( |
| PageEndReason page_end_reason, |
| UserInitiatedInfo user_initiated_info, |
| base::TimeTicks timestamp, |
| bool is_certainly_browser_timestamp) { |
| // When a provisional navigation commits, that navigation's start time is |
| // interpreted as the abort time for other provisional loads in the tab. |
| // However, this only makes sense if the committed load started after the |
| // aborted provisional loads started. Thus we ignore cases where the committed |
| // load started before the aborted provisional load, as this would result in |
| // recording a negative time-to-abort. The real issue here is that we have to |
| // infer the cause of aborts. It would be better if the navigation code could |
| // instead report the actual cause of an aborted navigation. See crbug/571647 |
| // for details. |
| if (timestamp < navigation_start_) { |
| RecordInternalError(ERR_END_BEFORE_NAVIGATION_START); |
| page_end_reason_ = END_NONE; |
| page_end_time_ = base::TimeTicks(); |
| return; |
| } |
| page_end_reason_ = page_end_reason; |
| page_end_time_ = timestamp; |
| // A client redirect can never be user initiated. Due to the way Blink |
| // implements user gesture tracking, where all events that occur within a few |
| // seconds after a user interaction are considered to be triggered by user |
| // activation (based on the HTML spec: |
| // https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation), |
| // these navs may sometimes be reported as user initiated by Blink. Thus, we |
| // explicitly filter these types of aborts out when deciding if the abort was |
| // user initiated. |
| if (page_end_reason != END_CLIENT_REDIRECT) { |
| page_end_user_initiated_info_ = user_initiated_info; |
| } |
| |
| if (is_certainly_browser_timestamp) { |
| ClampBrowserTimestampIfInterProcessTimeTickSkew(&page_end_time_); |
| } |
| if (page_end_reason_ == END_RENDER_PROCESS_GONE) { |
| for (const auto& observer : observers_) { |
| observer->OnPrimaryPageRenderProcessGone(); |
| } |
| } |
| } |
| |
| void PageLoadTracker::MediaStartedPlaying( |
| const content::WebContentsObserver::MediaPlayerInfo& video_type, |
| content::RenderFrameHost* render_frame_host) { |
| for (const auto& observer : observers_) { |
| observer->MediaStartedPlaying(video_type, render_frame_host); |
| } |
| } |
| |
| bool PageLoadTracker::IsPageMainFrame(content::RenderFrameHost* rfh) const { |
| DCHECK(page_main_frame_id_); |
| return rfh->GetGlobalId() == page_main_frame_id_; |
| } |
| |
| void PageLoadTracker::OnTimingChanged() { |
| DCHECK(!last_dispatched_merged_page_timing_->Equals( |
| metrics_update_dispatcher_.timing())); |
| |
| const mojom::PageLoadTiming& new_timing = metrics_update_dispatcher_.timing(); |
| |
| if (new_timing.activation_start && |
| !last_dispatched_merged_page_timing_->activation_start && |
| prerendering_state_ != PrerenderingState::kNoPrerendering) { |
| CHECK_EQ(prerendering_state_, |
| PrerenderingState::kActivatedNoActivationStart); |
| prerendering_state_ = PrerenderingState::kActivated; |
| activation_start_ = new_timing.activation_start; |
| } |
| |
| const mojom::PaintTimingPtr& paint_timing = |
| metrics_update_dispatcher_.timing().paint_timing; |
| |
| largest_contentful_paint_handler_.RecordMainFrameTiming( |
| *paint_timing->largest_contentful_paint, |
| paint_timing->first_input_or_scroll_notified_timestamp); |
| experimental_largest_contentful_paint_handler_.RecordMainFrameTiming( |
| *paint_timing->experimental_largest_contentful_paint, |
| paint_timing->first_input_or_scroll_notified_timestamp); |
| |
| for (const auto& observer : observers_) { |
| DispatchObserverTimingCallbacks( |
| observer.get(), *last_dispatched_merged_page_timing_, new_timing); |
| } |
| last_dispatched_merged_page_timing_ = |
| metrics_update_dispatcher_.timing().Clone(); |
| } |
| |
| void PageLoadTracker::OnPageInputTimingChanged(uint64_t num_interactions) { |
| for (const auto& observer : observers_) { |
| observer->OnPageInputTimingUpdate(num_interactions); |
| } |
| } |
| |
| void PageLoadTracker::OnSubFrameTimingChanged( |
| content::RenderFrameHost* rfh, |
| const mojom::PageLoadTiming& timing) { |
| DCHECK(rfh->GetParentOrOuterDocument()); |
| const mojom::PaintTimingPtr& paint_timing = timing.paint_timing; |
| largest_contentful_paint_handler_.RecordSubFrameTiming( |
| *paint_timing->largest_contentful_paint, |
| paint_timing->first_input_or_scroll_notified_timestamp, rfh, url_); |
| experimental_largest_contentful_paint_handler_.RecordSubFrameTiming( |
| *paint_timing->experimental_largest_contentful_paint, |
| paint_timing->first_input_or_scroll_notified_timestamp, rfh, url_); |
| for (const auto& observer : observers_) { |
| observer->OnTimingUpdate(rfh, timing); |
| } |
| } |
| |
| void PageLoadTracker::OnSubFrameInputTimingChanged( |
| content::RenderFrameHost* rfh, |
| const mojom::InputTiming& input_timing_delta) { |
| DCHECK(rfh->GetParentOrOuterDocument()); |
| for (const auto& observer : observers_) { |
| observer->OnInputTimingUpdate(rfh, input_timing_delta); |
| } |
| } |
| |
| void PageLoadTracker::OnPageRenderDataChanged( |
| const mojom::FrameRenderDataUpdate& render_data, |
| bool is_main_frame) { |
| for (const auto& observer : observers_) { |
| observer->OnPageRenderDataUpdate(render_data, is_main_frame); |
| } |
| } |
| |
| void PageLoadTracker::OnSubFrameRenderDataChanged( |
| content::RenderFrameHost* rfh, |
| const mojom::FrameRenderDataUpdate& render_data) { |
| DCHECK(rfh->GetParentOrOuterDocument()); |
| for (const auto& observer : observers_) { |
| observer->OnSubFrameRenderDataUpdate(rfh, render_data); |
| } |
| } |
| |
| void PageLoadTracker::OnMainFrameMetadataChanged() { |
| for (const auto& observer : observers_) { |
| observer->OnLoadingBehaviorObserved(nullptr, |
| GetMainFrameMetadata().behavior_flags); |
| observer->OnJavaScriptFrameworksObserved( |
| nullptr, GetMainFrameMetadata().framework_detection_result); |
| } |
| } |
| |
| void PageLoadTracker::OnSubframeMetadataChanged( |
| content::RenderFrameHost* rfh, |
| const mojom::FrameMetadata& metadata) { |
| for (const auto& observer : observers_) { |
| observer->OnLoadingBehaviorObserved(rfh, metadata.behavior_flags); |
| } |
| } |
| |
| void PageLoadTracker::OnSoftNavigationChanged( |
| const mojom::SoftNavigationMetrics& new_soft_navigation_metrics) { |
| if (new_soft_navigation_metrics.Equals(*soft_navigation_metrics_)) { |
| return; |
| } |
| |
| // TODO(crbug.com/40065440): For soft navigation detections, the count and |
| // start time should be monotonically increasing and navigation id different |
| // each time. But we do see check failures on |
| // soft_navigation_metrics.count >= soft_navigation_metrics_->count when this |
| // OnSoftNavigationChanged is only invoked by soft navigation detection. |
| // we should investigate this issue. |
| |
| for (const auto& observer : observers_) { |
| observer->OnSoftNavigationUpdated(new_soft_navigation_metrics); |
| } |
| |
| largest_contentful_paint_handler_.UpdateSoftNavigationLargestContentfulPaint( |
| *new_soft_navigation_metrics.largest_contentful_paint); |
| |
| // Reset the soft_navigation_interval_responsiveness_metrics_normalization_ |
| // when a new soft nav comes in. |
| if (new_soft_navigation_metrics.count > soft_navigation_metrics_->count) { |
| metrics_update_dispatcher_ |
| .ResetSoftNavigationIntervalResponsivenessMetricsNormalization(); |
| metrics_update_dispatcher_.ResetSoftNavigationIntervalLayoutShift(); |
| } |
| |
| soft_navigation_metrics_ = new_soft_navigation_metrics.Clone(); |
| } |
| |
| void PageLoadTracker::OnPrefetchLikely() { |
| for (const auto& observer : observers_) { |
| observer->OnPrefetchLikely(); |
| } |
| } |
| |
| void PageLoadTracker::UpdateFeaturesUsage( |
| content::RenderFrameHost* rfh, |
| const std::vector<blink::UseCounterFeature>& new_features) { |
| for (const auto& observer : observers_) { |
| observer->OnFeaturesUsageObserved(rfh, new_features); |
| } |
| } |
| |
| void PageLoadTracker::SetUpSharedMemoryForDroppedFrames( |
| base::ReadOnlySharedMemoryRegion dropped_frames_memory) { |
| DCHECK(dropped_frames_memory.IsValid()); |
| for (auto& observer : observers_) { |
| observer->SetUpSharedMemoryForDroppedFrames(dropped_frames_memory); |
| } |
| } |
| |
| void PageLoadTracker::UpdateResourceDataUse( |
| content::RenderFrameHost* rfh, |
| const std::vector<mojom::ResourceDataUpdatePtr>& resources) { |
| resource_tracker_.UpdateResourceDataUse(rfh->GetProcess()->GetDeprecatedID(), |
| resources); |
| for (const auto& observer : observers_) { |
| observer->OnResourceDataUseObserved(rfh, resources); |
| } |
| } |
| |
| void PageLoadTracker::UpdateFrameCpuTiming(content::RenderFrameHost* rfh, |
| const mojom::CpuTiming& timing) { |
| for (const auto& observer : observers_) { |
| observer->OnCpuTimingUpdate(rfh, timing); |
| } |
| } |
| |
| void PageLoadTracker::OnMainFrameIntersectionRectChanged( |
| content::RenderFrameHost* rfh, |
| const gfx::Rect& main_frame_intersection_rect) { |
| for (const auto& observer : observers_) { |
| observer->OnMainFrameIntersectionRectChanged(rfh, |
| main_frame_intersection_rect); |
| } |
| } |
| |
| void PageLoadTracker::OnMainFrameViewportRectChanged( |
| const gfx::Rect& main_frame_viewport_rect) { |
| for (const auto& observer : observers_) { |
| observer->OnMainFrameViewportRectChanged(main_frame_viewport_rect); |
| } |
| } |
| |
| void PageLoadTracker::OnMainFrameAdRectsChanged( |
| const base::flat_map<int, gfx::Rect>& main_frame_ad_rects) { |
| for (const auto& observer : observers_) { |
| observer->OnMainFrameAdRectsChanged(main_frame_ad_rects); |
| } |
| } |
| |
| content::WebContents* PageLoadTracker::GetWebContents() const { |
| return web_contents_; |
| } |
| |
| base::TimeTicks PageLoadTracker::GetNavigationStart() const { |
| return navigation_start_; |
| } |
| |
| std::optional<base::TimeDelta> |
| PageLoadTracker::DurationSinceNavigationStartForTime( |
| const std::optional<base::TimeTicks>& time) const { |
| std::optional<base::TimeDelta> duration; |
| |
| if (!time.has_value()) { |
| return duration; |
| } |
| |
| DCHECK_GE(time.value(), navigation_start_); |
| duration = time.value() - navigation_start_; |
| return duration; |
| } |
| |
| std::optional<base::TimeDelta> PageLoadTracker::GetTimeToFirstBackground() |
| const { |
| return DurationSinceNavigationStartForTime(first_background_time_); |
| } |
| |
| std::optional<base::TimeDelta> PageLoadTracker::GetTimeToFirstForeground() |
| const { |
| return DurationSinceNavigationStartForTime(first_foreground_time_); |
| } |
| |
| const PageLoadMetricsObserverDelegate::BackForwardCacheRestore& |
| PageLoadTracker::GetBackForwardCacheRestore(size_t index) const { |
| return back_forward_cache_restores_[index]; |
| } |
| |
| bool PageLoadTracker::StartedInForeground() const { |
| return started_in_foreground_; |
| } |
| |
| PageVisibility PageLoadTracker::GetVisibilityAtActivation() const { |
| return visibility_at_activation_; |
| } |
| |
| bool PageLoadTracker::WasPrerenderedThenActivatedInForeground() const { |
| return GetVisibilityAtActivation() == PageVisibility::kForeground; |
| } |
| |
| PrerenderingState PageLoadTracker::GetPrerenderingState() const { |
| return prerendering_state_; |
| } |
| |
| std::optional<base::TimeDelta> PageLoadTracker::GetActivationStart() const { |
| return activation_start_; |
| } |
| |
| const UserInitiatedInfo& PageLoadTracker::GetUserInitiatedInfo() const { |
| return user_initiated_info_; |
| } |
| |
| const GURL& PageLoadTracker::GetUrl() const { |
| return url(); |
| } |
| |
| const GURL& PageLoadTracker::GetStartUrl() const { |
| return start_url_; |
| } |
| |
| bool PageLoadTracker::DidCommit() const { |
| return did_commit_; |
| } |
| |
| PageEndReason PageLoadTracker::GetPageEndReason() const { |
| return page_end_reason_; |
| } |
| |
| const UserInitiatedInfo& PageLoadTracker::GetPageEndUserInitiatedInfo() const { |
| return page_end_user_initiated_info_; |
| } |
| |
| std::optional<base::TimeDelta> PageLoadTracker::GetTimeToPageEnd() const { |
| if (page_end_reason_ != END_NONE) { |
| return DurationSinceNavigationStartForTime(page_end_time_); |
| } |
| DCHECK(page_end_time_.is_null()); |
| return std::nullopt; |
| } |
| |
| const base::TimeTicks& PageLoadTracker::GetPageEndTime() const { |
| return page_end_time_; |
| } |
| |
| const mojom::FrameMetadata& PageLoadTracker::GetMainFrameMetadata() const { |
| return metrics_update_dispatcher_.main_frame_metadata(); |
| } |
| |
| const mojom::FrameMetadata& PageLoadTracker::GetSubframeMetadata() const { |
| return metrics_update_dispatcher_.subframe_metadata(); |
| } |
| |
| const PageRenderData& PageLoadTracker::GetPageRenderData() const { |
| return metrics_update_dispatcher_.page_render_data(); |
| } |
| |
| const NormalizedCLSData& PageLoadTracker::GetNormalizedCLSData( |
| BfcacheStrategy bfcache_strategy) const { |
| return metrics_update_dispatcher_.normalized_cls_data(bfcache_strategy); |
| } |
| |
| const NormalizedCLSData& |
| PageLoadTracker::GetSoftNavigationIntervalNormalizedCLSData() const { |
| return metrics_update_dispatcher_ |
| .soft_navigation_interval_normalized_layout_shift(); |
| } |
| |
| const ResponsivenessMetricsNormalization& |
| PageLoadTracker::GetResponsivenessMetricsNormalization() const { |
| return metrics_update_dispatcher_.responsiveness_metrics_normalization(); |
| } |
| |
| const ResponsivenessMetricsNormalization& |
| PageLoadTracker::GetSoftNavigationIntervalResponsivenessMetricsNormalization() |
| const { |
| return metrics_update_dispatcher_ |
| .soft_navigation_interval_responsiveness_metrics_normalization(); |
| } |
| |
| const mojom::InputTiming& PageLoadTracker::GetPageInputTiming() const { |
| return metrics_update_dispatcher_.page_input_timing(); |
| } |
| |
| const std::optional<blink::SubresourceLoadMetrics>& |
| PageLoadTracker::GetSubresourceLoadMetrics() const { |
| return metrics_update_dispatcher_.subresource_load_metrics(); |
| } |
| |
| const PageRenderData& PageLoadTracker::GetMainFrameRenderData() const { |
| return metrics_update_dispatcher_.main_frame_render_data(); |
| } |
| |
| const ui::ScopedVisibilityTracker& PageLoadTracker::GetVisibilityTracker() |
| const { |
| return visibility_tracker_; |
| } |
| |
| const ResourceTracker& PageLoadTracker::GetResourceTracker() const { |
| return resource_tracker_; |
| } |
| |
| const LargestContentfulPaintHandler& |
| PageLoadTracker::GetLargestContentfulPaintHandler() const { |
| return largest_contentful_paint_handler_; |
| } |
| |
| const LargestContentfulPaintHandler& |
| PageLoadTracker::GetExperimentalLargestContentfulPaintHandler() const { |
| return experimental_largest_contentful_paint_handler_; |
| } |
| |
| ukm::SourceId PageLoadTracker::GetPageUkmSourceId() const { |
| DCHECK_NE(ukm::kInvalidSourceId, source_id_) |
| << "GetPageUkmSourceId was called on a prerendered page before its " |
| "activation. We should not collect UKM while prerendering pages."; |
| return source_id_; |
| } |
| |
| mojom::SoftNavigationMetrics& PageLoadTracker::GetSoftNavigationMetrics() |
| const { |
| return *soft_navigation_metrics_; |
| } |
| |
| ukm::SourceId PageLoadTracker::GetUkmSourceIdForSoftNavigation() const { |
| return potential_soft_navigation_source_id_; |
| } |
| |
| ukm::SourceId PageLoadTracker::GetPreviousUkmSourceIdForSoftNavigation() const { |
| return previous_soft_navigation_source_id_; |
| } |
| |
| bool PageLoadTracker::IsFirstNavigationInWebContents() const { |
| return is_first_navigation_in_web_contents_; |
| } |
| |
| bool PageLoadTracker::IsOriginVisit() const { |
| return is_origin_visit_; |
| } |
| |
| bool PageLoadTracker::IsTerminalVisit() const { |
| return is_terminal_visit_; |
| } |
| |
| bool PageLoadTracker::ShouldObserveScheme(std::string_view scheme) const { |
| return embedder_interface_->ShouldObserveScheme(scheme); |
| } |
| |
| int64_t PageLoadTracker::GetNavigationId() const { |
| return navigation_id_; |
| } |
| |
| void PageLoadTracker::RecordLinkNavigation() { |
| is_terminal_visit_ = false; |
| } |
| |
| void PageLoadTracker::OnEnterBackForwardCache() { |
| // In case of BackForwardCache, invoke and update the |
| // PageLoadMetricsUpdateDispatcher before the page is hidden to enable |
| // recording metrics that requires the page to be in foreground before |
| // entering BackForwardCache on navigation. |
| InvokeAndPruneObservers( |
| "PageLoadMetricsObserver::OnEnterBackForwardCache", |
| base::BindRepeating( |
| [](const mojom::PageLoadTiming* timing, |
| PageLoadMetricsObserverInterface* observer) { |
| return observer->OnEnterBackForwardCache(*timing); |
| }, |
| &metrics_update_dispatcher_.timing()), |
| /*permit_forwarding=*/false); |
| metrics_update_dispatcher_.UpdateLayoutShiftNormalizationForBfcache(); |
| metrics_update_dispatcher_ |
| .UpdateResponsivenessMetricsNormalizationForBfcache(); |
| if (GetWebContents()->GetVisibility() == content::Visibility::VISIBLE) { |
| PageHidden(); |
| } |
| } |
| |
| void PageLoadTracker::OnRestoreFromBackForwardCache( |
| content::NavigationHandle* navigation_handle) { |
| DCHECK(!visibility_tracker_.currently_in_foreground()); |
| bool visible = |
| GetWebContents()->GetVisibility() == content::Visibility::VISIBLE; |
| |
| BackForwardCacheRestore back_forward_cache_restore( |
| visible, navigation_handle->NavigationStart()); |
| back_forward_cache_restores_.push_back(back_forward_cache_restore); |
| |
| if (visible) { |
| PageShown(); |
| } |
| |
| for (const auto& observer : observers_) { |
| observer->OnRestoreFromBackForwardCache(metrics_update_dispatcher_.timing(), |
| navigation_handle); |
| } |
| |
| // Reset the page end reason to END_NONE. The page has been restored, its |
| // previous end reason is no longer relevant. Similarly, its page end time is |
| // no longer accurate, so reset that as well. |
| page_end_reason_ = END_NONE; |
| page_end_time_ = base::TimeTicks(); |
| } |
| |
| void PageLoadTracker::OnV8MemoryChanged( |
| const std::vector<MemoryUpdate>& memory_updates) { |
| for (const auto& observer : observers_) { |
| observer->OnV8MemoryChanged(memory_updates); |
| } |
| } |
| |
| void PageLoadTracker::OnSharedStorageWorkletHostCreated() { |
| for (const auto& observer : observers_) { |
| observer->OnSharedStorageWorkletHostCreated(); |
| } |
| } |
| |
| void PageLoadTracker::OnSharedStorageSelectURLCalled() { |
| for (const auto& observer : observers_) { |
| observer->OnSharedStorageSelectURLCalled(); |
| } |
| } |
| |
| void PageLoadTracker::OnAdAuctionComplete(bool is_server_auction, |
| bool is_on_device_auction, |
| content::AuctionResult result) { |
| for (const auto& observer : observers_) { |
| observer->OnAdAuctionComplete(is_server_auction, is_on_device_auction, |
| result); |
| } |
| } |
| |
| void PageLoadTracker::UpdateMetrics( |
| content::RenderFrameHost* render_frame_host, |
| mojom::PageLoadTimingPtr timing, |
| mojom::FrameMetadataPtr metadata, |
| const std::vector<blink::UseCounterFeature>& 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 (parent_tracker_) { |
| parent_tracker_->UpdateMetrics( |
| render_frame_host, timing.Clone(), metadata.Clone(), features, |
| resources, render_data.Clone(), cpu_timing.Clone(), |
| input_timing_delta.Clone(), subresource_load_metrics, |
| soft_navigation_metrics.Clone()); |
| } |
| |
| metrics_update_dispatcher_.UpdateMetrics( |
| render_frame_host, std::move(timing), std::move(metadata), |
| std::move(features), resources, std::move(render_data), |
| std::move(cpu_timing), std::move(input_timing_delta), |
| subresource_load_metrics, std::move(soft_navigation_metrics), page_type_); |
| } |
| |
| void PageLoadTracker::AddCustomUserTimings( |
| std::vector<mojom::CustomUserTimingMarkPtr> custom_timings) { |
| for (const auto& observer : observers_) { |
| observer->OnCustomUserTimingMarkObserved(custom_timings); |
| } |
| } |
| |
| void PageLoadTracker::SetPageMainFrame(content::RenderFrameHost* rfh) { |
| page_main_frame_id_ = rfh->GetGlobalId(); |
| } |
| |
| base::WeakPtr<PageLoadTracker> PageLoadTracker::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void PageLoadTracker::InvokeAndPruneObservers( |
| const char* trace_name, |
| PageLoadTracker::InvokeCallback callback, |
| bool permit_forwarding) { |
| TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), trace_name); |
| std::vector<std::unique_ptr<PageLoadMetricsObserverInterface>> |
| forward_observers; |
| for (auto it = observers_.begin(); it != observers_.end();) { |
| auto policy = callback.Run(it->get()); |
| switch (policy) { |
| case PageLoadMetricsObserver::CONTINUE_OBSERVING: |
| ++it; |
| break; |
| case PageLoadMetricsObserver::STOP_OBSERVING: |
| if ((*it)->GetObserverName()) { |
| observers_map_.erase((*it)->GetObserverName()); |
| } |
| it = observers_.erase(it); |
| break; |
| case PageLoadMetricsObserver::FORWARD_OBSERVING: |
| DCHECK(permit_forwarding); |
| DCHECK((*it)->GetObserverName()) |
| << "GetObserverName should be implemented"; |
| auto target_observer = |
| parent_tracker_ |
| ? parent_tracker_->FindObserver((*it)->GetObserverName()) |
| : nullptr; |
| if (target_observer) { |
| forward_observers.emplace_back( |
| std::make_unique<PageLoadMetricsForwardObserver>( |
| target_observer)); |
| } |
| observers_map_.erase((*it)->GetObserverName()); |
| it = observers_.erase(it); |
| break; |
| } |
| } |
| for (auto& observer : forward_observers) { |
| DCHECK(observers_map_.find(observer->GetObserverName()) == |
| observers_map_.end()); |
| AddObserver(std::move(observer)); |
| } |
| } |
| |
| } // namespace page_load_metrics |