| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // 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 <string> |
| #include <utility> |
| |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/user_metrics.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_util.h" |
| #include "components/page_load_metrics/common/page_load_metrics_messages.h" |
| #include "components/page_load_metrics/common/page_load_timing.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_details.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_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 "ipc/ipc_message.h" |
| #include "ipc/ipc_message_macros.h" |
| #include "ui/base/page_transition_types.h" |
| |
| DEFINE_WEB_CONTENTS_USER_DATA_KEY( |
| page_load_metrics::MetricsWebContentsObserver); |
| |
| namespace page_load_metrics { |
| |
| namespace internal { |
| |
| const char kErrorEvents[] = "PageLoad.Events.InternalError"; |
| const char kAbortChainSizeReload[] = |
| "PageLoad.Internal.ProvisionalAbortChainSize.Reload"; |
| const char kAbortChainSizeForwardBack[] = |
| "PageLoad.Internal.ProvisionalAbortChainSize.ForwardBack"; |
| const char kAbortChainSizeNewNavigation[] = |
| "PageLoad.Internal.ProvisionalAbortChainSize.NewNavigation"; |
| const char kAbortChainSizeSameURL[] = |
| "PageLoad.Internal.ProvisionalAbortChainSize.SameURL"; |
| const char kAbortChainSizeNoCommit[] = |
| "PageLoad.Internal.ProvisionalAbortChainSize.NoCommit"; |
| |
| } // namespace internal |
| |
| namespace { |
| |
| // The url we see from the renderer side is not always the same as what |
| // we see from the browser side (e.g. chrome://newtab). We want to be |
| // sure here that we aren't logging UMA for internal pages. |
| bool IsRelevantNavigation(content::NavigationHandle* navigation_handle, |
| const GURL& browser_url, |
| const std::string& mime_type) { |
| DCHECK(navigation_handle->HasCommitted()); |
| return navigation_handle->IsInMainFrame() && |
| !navigation_handle->IsSamePage() && |
| !navigation_handle->IsErrorPage() && |
| navigation_handle->GetURL().SchemeIsHTTPOrHTTPS() && |
| browser_url.SchemeIsHTTPOrHTTPS() && |
| (mime_type == "text/html" || mime_type == "application/xhtml+xml"); |
| } |
| |
| // If second is non-zero, first must also be non-zero and less than or equal to |
| // second. |
| bool EventsInOrder(base::TimeDelta first, base::TimeDelta second) { |
| if (second.is_zero()) { |
| return true; |
| } |
| return !first.is_zero() && first <= second; |
| } |
| |
| bool IsValidPageLoadTiming(const PageLoadTiming& timing) { |
| if (timing.IsEmpty()) |
| return false; |
| |
| // If we have a non-empty timing, it should always have a navigation start. |
| if (timing.navigation_start.is_null()) { |
| NOTREACHED() << "Received null navigation_start."; |
| return false; |
| } |
| |
| // Verify proper ordering between the various timings. |
| |
| if (!EventsInOrder(timing.response_start, timing.dom_loading)) { |
| // We sometimes get a zero response_start with a non-zero DOM loading. See |
| // crbug.com/590212. |
| DLOG(ERROR) << "Invalid response_start " << timing.response_start |
| << " for dom_loading " << timing.dom_loading; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.response_start, timing.parse_start)) { |
| // We sometimes get a zero response_start with a non-zero parse start. See |
| // crbug.com/590212. |
| DLOG(ERROR) << "Invalid response_start " << timing.response_start |
| << " for parse_start " << timing.parse_start; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.parse_start, timing.parse_stop)) { |
| NOTREACHED() << "Invalid parse_start " << timing.parse_start |
| << " for parse_stop " << timing.parse_stop; |
| return false; |
| } |
| |
| if (!timing.parse_stop.is_zero()) { |
| const base::TimeDelta parse_duration = |
| timing.parse_stop - timing.parse_start; |
| if (timing.parse_blocked_on_script_load_duration > parse_duration) { |
| NOTREACHED() << "Invalid parse_blocked_on_script_load_duration " |
| << timing.parse_blocked_on_script_load_duration |
| << " for parse duration " << parse_duration; |
| return false; |
| } |
| } |
| |
| if (timing.parse_blocked_on_script_load_from_document_write_duration > |
| timing.parse_blocked_on_script_load_duration) { |
| NOTREACHED() |
| << "Invalid parse_blocked_on_script_load_from_document_write_duration " |
| << timing.parse_blocked_on_script_load_from_document_write_duration |
| << " for parse_blocked_on_script_load_duration " |
| << timing.parse_blocked_on_script_load_duration; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.dom_loading, |
| timing.dom_content_loaded_event_start)) { |
| NOTREACHED() << "Invalid dom_loading " << timing.dom_loading |
| << " for dom_content_loaded_event_start " |
| << timing.dom_content_loaded_event_start; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.dom_content_loaded_event_start, |
| timing.load_event_start)) { |
| NOTREACHED() << "Invalid dom_content_loaded_event_start " |
| << timing.dom_content_loaded_event_start |
| << " for load_event_start " << timing.load_event_start; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.dom_loading, timing.first_layout)) { |
| NOTREACHED() << "Invalid dom_loading " << timing.dom_loading |
| << " for first_layout " << timing.first_layout; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.first_layout, timing.first_paint)) { |
| NOTREACHED() << "Invalid first_layout " << timing.first_layout |
| << " for first_paint " << timing.first_paint; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.first_paint, timing.first_text_paint)) { |
| NOTREACHED() << "Invalid first_paint " << timing.first_paint |
| << " for first_text_paint " << timing.first_text_paint; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.first_paint, timing.first_image_paint)) { |
| NOTREACHED() << "Invalid first_paint " << timing.first_paint |
| << " for first_image_paint " << timing.first_image_paint; |
| return false; |
| } |
| |
| if (!EventsInOrder(timing.first_paint, timing.first_contentful_paint)) { |
| NOTREACHED() << "Invalid first_paint " << timing.first_paint |
| << " for first_contentful_paint " |
| << timing.first_contentful_paint; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void RecordInternalError(InternalErrorLoadEvent event) { |
| UMA_HISTOGRAM_ENUMERATION(internal::kErrorEvents, event, ERR_LAST_ENTRY); |
| } |
| |
| UserAbortType AbortTypeForPageTransition(ui::PageTransition transition) { |
| if (ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_RELOAD)) |
| return ABORT_RELOAD; |
| if (transition & ui::PAGE_TRANSITION_FORWARD_BACK) |
| return ABORT_FORWARD_BACK; |
| if (ui::PageTransitionIsNewNavigation(transition)) |
| return ABORT_NEW_NAVIGATION; |
| NOTREACHED() |
| << "AbortTypeForPageTransition received unexpected ui::PageTransition: " |
| << transition; |
| return ABORT_OTHER; |
| } |
| |
| void LogAbortChainSameURLHistogram(int aborted_chain_size_same_url) { |
| if (aborted_chain_size_same_url > 0) { |
| UMA_HISTOGRAM_COUNTS(internal::kAbortChainSizeSameURL, |
| aborted_chain_size_same_url); |
| } |
| } |
| |
| void DispatchObserverTimingCallbacks(PageLoadMetricsObserver* observer, |
| const PageLoadTiming& last_timing, |
| const PageLoadTiming& new_timing, |
| const PageLoadExtraInfo& extra_info) { |
| observer->OnTimingUpdate(new_timing, extra_info); |
| if (!new_timing.dom_content_loaded_event_start.is_zero() && |
| last_timing.dom_content_loaded_event_start.is_zero()) |
| observer->OnDomContentLoadedEventStart(new_timing, extra_info); |
| if (!new_timing.load_event_start.is_zero() && |
| last_timing.load_event_start.is_zero()) |
| observer->OnLoadEventStart(new_timing, extra_info); |
| if (!new_timing.first_layout.is_zero() && last_timing.first_layout.is_zero()) |
| observer->OnFirstLayout(new_timing, extra_info); |
| if (!new_timing.first_paint.is_zero() && last_timing.first_paint.is_zero()) |
| observer->OnFirstPaint(new_timing, extra_info); |
| if (!new_timing.first_text_paint.is_zero() && |
| last_timing.first_text_paint.is_zero()) |
| observer->OnFirstTextPaint(new_timing, extra_info); |
| if (!new_timing.first_image_paint.is_zero() && |
| last_timing.first_image_paint.is_zero()) |
| observer->OnFirstImagePaint(new_timing, extra_info); |
| if (!new_timing.first_contentful_paint.is_zero() && |
| last_timing.first_contentful_paint.is_zero()) |
| observer->OnFirstContentfulPaint(new_timing, extra_info); |
| if (!new_timing.parse_start.is_zero() && last_timing.parse_start.is_zero()) |
| observer->OnParseStart(new_timing, extra_info); |
| if (!new_timing.parse_stop.is_zero() && last_timing.parse_stop.is_zero()) |
| observer->OnParseStop(new_timing, extra_info); |
| } |
| |
| } // namespace |
| |
| PageLoadTracker::PageLoadTracker( |
| bool in_foreground, |
| PageLoadMetricsEmbedderInterface* embedder_interface, |
| const GURL& currently_committed_url, |
| content::NavigationHandle* navigation_handle, |
| int aborted_chain_size, |
| int aborted_chain_size_same_url) |
| : renderer_tracked_(false), |
| navigation_start_(navigation_handle->NavigationStart()), |
| url_(navigation_handle->GetURL()), |
| abort_type_(ABORT_NONE), |
| started_in_foreground_(in_foreground), |
| aborted_chain_size_(aborted_chain_size), |
| aborted_chain_size_same_url_(aborted_chain_size_same_url), |
| embedder_interface_(embedder_interface) { |
| DCHECK(!navigation_handle->HasCommitted()); |
| embedder_interface_->RegisterObservers(this); |
| for (const auto& observer : observers_) { |
| observer->OnStart(navigation_handle, currently_committed_url, |
| started_in_foreground_); |
| } |
| } |
| |
| PageLoadTracker::~PageLoadTracker() { |
| const PageLoadExtraInfo info = GetPageLoadMetricsInfo(); |
| if (!info.time_to_commit.is_zero() && renderer_tracked() && |
| timing_.IsEmpty()) { |
| RecordInternalError(ERR_NO_IPCS_RECEIVED); |
| } |
| // Recall that trackers that are given ABORT_UNKNOWN_NAVIGATION have their |
| // chain length added to the next navigation. Take care not to double count |
| // them. Also do not double count committed loads, which call this already. |
| if (commit_time_.is_null() && abort_type_ != ABORT_UNKNOWN_NAVIGATION) |
| LogAbortChainHistograms(nullptr); |
| |
| for (const auto& observer : observers_) { |
| observer->OnComplete(timing_, info); |
| } |
| } |
| |
| void PageLoadTracker::LogAbortChainHistograms( |
| content::NavigationHandle* final_navigation) { |
| if (aborted_chain_size_ == 0) |
| return; |
| // Note that this could be broken out by this navigation's abort type, if more |
| // granularity is needed. Add one to the chain size to count the current |
| // navigation. In the other cases, the current navigation is the final |
| // navigation (which commits). |
| if (!final_navigation) { |
| UMA_HISTOGRAM_COUNTS(internal::kAbortChainSizeNoCommit, |
| aborted_chain_size_ + 1); |
| LogAbortChainSameURLHistogram(aborted_chain_size_same_url_ + 1); |
| return; |
| } |
| |
| // The following is only executed for committing trackers. |
| DCHECK(!commit_time_.is_null()); |
| |
| // Note that histograms could be separated out by this commit's transition |
| // type, but for simplicity they will all be bucketed together. |
| LogAbortChainSameURLHistogram(aborted_chain_size_same_url_); |
| |
| ui::PageTransition committed_transition = |
| final_navigation->GetPageTransition(); |
| switch (AbortTypeForPageTransition(committed_transition)) { |
| case ABORT_RELOAD: |
| UMA_HISTOGRAM_COUNTS(internal::kAbortChainSizeReload, |
| aborted_chain_size_); |
| return; |
| case ABORT_FORWARD_BACK: |
| UMA_HISTOGRAM_COUNTS(internal::kAbortChainSizeForwardBack, |
| aborted_chain_size_); |
| return; |
| case ABORT_NEW_NAVIGATION: |
| UMA_HISTOGRAM_COUNTS(internal::kAbortChainSizeNewNavigation, |
| aborted_chain_size_); |
| return; |
| default: |
| NOTREACHED() |
| << "LogAbortChainHistograms received unexpected ui::PageTransition: " |
| << committed_transition; |
| return; |
| } |
| } |
| |
| void PageLoadTracker::WebContentsHidden() { |
| // Only log the first time we background in a given page load. |
| if (background_time_.is_null()) { |
| // Make sure we either started in the foreground and haven't been |
| // foregrounded yet, or started in the background and have already been |
| // foregrounded. |
| DCHECK_EQ(started_in_foreground_, foreground_time_.is_null()); |
| background_time_ = base::TimeTicks::Now(); |
| } |
| |
| for (const auto& observer : observers_) |
| observer->OnHidden(); |
| } |
| |
| void PageLoadTracker::WebContentsShown() { |
| // Only log the first time we foreground in a given page load. |
| if (foreground_time_.is_null()) { |
| // Make sure we either started in the background and haven't been |
| // backgrounded yet, or started in the foreground and have already been |
| // backgrounded. |
| DCHECK_NE(started_in_foreground_, background_time_.is_null()); |
| foreground_time_ = base::TimeTicks::Now(); |
| } |
| |
| for (const auto& observer : observers_) |
| observer->OnShown(); |
| } |
| |
| void PageLoadTracker::Commit(content::NavigationHandle* navigation_handle) { |
| // TODO(bmcquade): To improve accuracy, consider adding commit time to |
| // NavigationHandle. Taking a timestamp here should be close enough for now. |
| commit_time_ = base::TimeTicks::Now(); |
| url_ = navigation_handle->GetURL(); |
| for (const auto& observer : observers_) { |
| observer->OnCommit(navigation_handle); |
| } |
| LogAbortChainHistograms(navigation_handle); |
| } |
| |
| void PageLoadTracker::FailedProvisionalLoad( |
| content::NavigationHandle* navigation_handle) { |
| for (const auto& observer : observers_) { |
| observer->OnFailedProvisionalLoad(navigation_handle); |
| } |
| } |
| |
| void PageLoadTracker::Redirect(content::NavigationHandle* navigation_handle) { |
| for (const auto& observer : observers_) { |
| observer->OnRedirect(navigation_handle); |
| } |
| } |
| |
| bool PageLoadTracker::UpdateTiming(const PageLoadTiming& new_timing, |
| const PageLoadMetadata& new_metadata) { |
| // Throw away IPCs that are not relevant to the current navigation. |
| // Two timing structures cannot refer to the same navigation if they indicate |
| // that a navigation started at different times, so a new timing struct with a |
| // different start time from an earlier struct is considered invalid. |
| bool valid_timing_descendent = |
| timing_.navigation_start.is_null() || |
| timing_.navigation_start == new_timing.navigation_start; |
| // Ensure flags sent previously are still present in the new metadata fields. |
| bool valid_behavior_descendent = |
| (metadata_.behavior_flags & new_metadata.behavior_flags) == |
| metadata_.behavior_flags; |
| if (IsValidPageLoadTiming(new_timing) && valid_timing_descendent && |
| valid_behavior_descendent) { |
| // There are some subtle ordering constraints here. GetPageLoadMetricsInfo() |
| // must be called before DispatchObserverTimingCallbacks, but its |
| // implementation depends on the state of metadata_, so we need to update |
| // metadata_ before calling GetPageLoadMetricsInfo. Thus, we make a copy of |
| // timing here, update timing_ and metadata_, and then proceed to dispatch |
| // the observer timing callbacks. |
| const PageLoadTiming last_timing = timing_; |
| timing_ = new_timing; |
| metadata_ = new_metadata; |
| const PageLoadExtraInfo info = GetPageLoadMetricsInfo(); |
| for (const auto& observer : observers_) { |
| DispatchObserverTimingCallbacks(observer.get(), last_timing, new_timing, |
| info); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| void PageLoadTracker::set_renderer_tracked(bool renderer_tracked) { |
| renderer_tracked_ = renderer_tracked; |
| } |
| |
| void PageLoadTracker::AddObserver( |
| std::unique_ptr<PageLoadMetricsObserver> observer) { |
| observers_.push_back(std::move(observer)); |
| } |
| |
| PageLoadExtraInfo PageLoadTracker::GetPageLoadMetricsInfo() { |
| base::TimeDelta first_background_time; |
| base::TimeDelta first_foreground_time; |
| base::TimeDelta time_to_abort; |
| base::TimeDelta time_to_commit; |
| if (!background_time_.is_null()) |
| first_background_time = background_time_ - navigation_start_; |
| if (!foreground_time_.is_null()) |
| first_foreground_time = foreground_time_ - navigation_start_; |
| if (abort_type_ != ABORT_NONE) { |
| DCHECK_GT(abort_time_, navigation_start_); |
| time_to_abort = abort_time_ - navigation_start_; |
| } else { |
| DCHECK(abort_time_.is_null()); |
| } |
| |
| if (!commit_time_.is_null()) { |
| DCHECK_GT(commit_time_, navigation_start_); |
| time_to_commit = commit_time_ - navigation_start_; |
| } else { |
| DCHECK(commit_time_.is_null()); |
| } |
| return PageLoadExtraInfo( |
| first_background_time, first_foreground_time, started_in_foreground_, |
| commit_time_.is_null() ? GURL() : url_, time_to_commit, abort_type_, |
| time_to_abort, metadata_); |
| } |
| |
| void PageLoadTracker::NotifyAbort(UserAbortType abort_type, |
| base::TimeTicks timestamp) { |
| DCHECK_NE(abort_type, ABORT_NONE); |
| // Use UpdateAbort to update an already notified PageLoadTracker. |
| if (abort_type_ != ABORT_NONE) |
| return; |
| |
| UpdateAbortInternal(abort_type, timestamp); |
| } |
| |
| void PageLoadTracker::UpdateAbort(UserAbortType abort_type, |
| base::TimeTicks timestamp) { |
| DCHECK_NE(abort_type, ABORT_NONE); |
| DCHECK_NE(abort_type, ABORT_OTHER); |
| DCHECK_EQ(abort_type_, ABORT_OTHER); |
| |
| // 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. |
| UpdateAbortInternal(abort_type, std::min(abort_time_, timestamp)); |
| } |
| |
| bool PageLoadTracker::IsLikelyProvisionalAbort( |
| base::TimeTicks abort_cause_time) { |
| // Note that |abort_cause_time - abort_time| can be negative. |
| return abort_type_ == ABORT_OTHER && |
| (abort_cause_time - abort_time_).InMilliseconds() < 100; |
| } |
| |
| bool PageLoadTracker::MatchesOriginalNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // Neither navigation should have committed. |
| DCHECK(!navigation_handle->HasCommitted()); |
| DCHECK(commit_time_.is_null()); |
| return navigation_handle->GetURL() == url_; |
| } |
| |
| void PageLoadTracker::UpdateAbortInternal(UserAbortType abort_type, |
| base::TimeTicks 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_ABORT_BEFORE_NAVIGATION_START); |
| abort_type_ = ABORT_NONE; |
| abort_time_ = base::TimeTicks(); |
| return; |
| } |
| abort_type_ = abort_type; |
| abort_time_ = timestamp; |
| } |
| |
| // static |
| MetricsWebContentsObserver::MetricsWebContentsObserver( |
| content::WebContents* web_contents, |
| std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface) |
| : content::WebContentsObserver(web_contents), |
| in_foreground_(false), |
| embedder_interface_(std::move(embedder_interface)), |
| has_navigated_(false) {} |
| |
| MetricsWebContentsObserver* MetricsWebContentsObserver::CreateForWebContents( |
| content::WebContents* web_contents, |
| std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface) { |
| DCHECK(web_contents); |
| |
| MetricsWebContentsObserver* metrics = FromWebContents(web_contents); |
| if (!metrics) { |
| metrics = new MetricsWebContentsObserver(web_contents, |
| std::move(embedder_interface)); |
| web_contents->SetUserData(UserDataKey(), metrics); |
| } |
| return metrics; |
| } |
| |
| MetricsWebContentsObserver::~MetricsWebContentsObserver() { |
| NotifyAbortAllLoads(ABORT_CLOSE); |
| } |
| |
| bool MetricsWebContentsObserver::OnMessageReceived( |
| const IPC::Message& message, |
| content::RenderFrameHost* render_frame_host) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(MetricsWebContentsObserver, message, |
| render_frame_host) |
| IPC_MESSAGE_HANDLER(PageLoadMetricsMsg_TimingUpdated, OnTimingUpdated) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| void MetricsWebContentsObserver::DidStartNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsInMainFrame()) |
| return; |
| if (embedder_interface_->IsPrerendering(web_contents())) |
| return; |
| if (navigation_handle->GetURL().spec().compare(url::kAboutBlankURL) == 0) |
| return; |
| |
| std::unique_ptr<PageLoadTracker> last_aborted = |
| NotifyAbortedProvisionalLoadsNewNavigation(navigation_handle); |
| |
| int chain_size_same_url = 0; |
| int chain_size = 0; |
| if (last_aborted) { |
| if (last_aborted->MatchesOriginalNavigation(navigation_handle)) { |
| chain_size_same_url = last_aborted->aborted_chain_size_same_url() + 1; |
| } else if (last_aborted->aborted_chain_size_same_url() > 0) { |
| LogAbortChainSameURLHistogram( |
| last_aborted->aborted_chain_size_same_url()); |
| } |
| chain_size = last_aborted->aborted_chain_size() + 1; |
| } |
| |
| // 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::WebContents* opener = web_contents()->GetOpener(); |
| const GURL& opener_url = |
| !has_navigated_ && opener |
| ? web_contents()->GetOpener()->GetLastCommittedURL() |
| : GURL::EmptyGURL(); |
| const GURL& currently_committed_url = |
| committed_load_ ? committed_load_->committed_url() : opener_url; |
| has_navigated_ = true; |
| |
| // We can have two provisional loads in some cases. E.g. a same-site |
| // navigation can have a concurrent cross-process navigation started |
| // from the omnibox. |
| DCHECK_GT(2ul, provisional_loads_.size()); |
| // Passing raw pointers to observers_ and 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 |
| // committed_load_ or navigation_handle beyond the scope of the constructor. |
| provisional_loads_.insert(std::make_pair( |
| navigation_handle, |
| base::WrapUnique(new PageLoadTracker( |
| in_foreground_, embedder_interface_.get(), currently_committed_url, |
| navigation_handle, chain_size, chain_size_same_url)))); |
| } |
| |
| void MetricsWebContentsObserver::DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsInMainFrame()) |
| return; |
| |
| std::unique_ptr<PageLoadTracker> finished_nav( |
| std::move(provisional_loads_[navigation_handle])); |
| provisional_loads_.erase(navigation_handle); |
| |
| // There's a chance a navigation could have started before we were added to a |
| // tab. Bail out early if this is the case. |
| if (!finished_nav) |
| return; |
| |
| // Handle a pre-commit error here. Navigations that result in an error page |
| // will be ignored. Note that downloads/204s will result in HasCommitted() |
| // returning false. |
| // TODO(csharrison): Track changes to NavigationHandle for signals when this |
| // is the case (HTTP response headers). |
| if (!navigation_handle->HasCommitted()) { |
| finished_nav->FailedProvisionalLoad(navigation_handle); |
| |
| net::Error error = navigation_handle->GetNetErrorCode(); |
| |
| // net::OK: This case occurs when the NavigationHandle finishes and reports |
| // !HasCommitted(), but reports no net::Error. This should not occur |
| // pre-PlzNavigate, but afterwards it should represent the navigation |
| // stopped by the user before it was ready to commit. |
| // net::ERR_ABORTED: An aborted provisional load has error net::ERR_ABORTED. |
| // Note that this can come from some non user-initiated errors, such as |
| // downloads, or 204 responses. See crbug.com/542369. |
| if ((error == net::OK) || (error == net::ERR_ABORTED)) { |
| finished_nav->NotifyAbort(ABORT_OTHER, base::TimeTicks::Now()); |
| aborted_provisional_loads_.push_back(std::move(finished_nav)); |
| } |
| |
| return; |
| } |
| |
| // Don't treat a same-page nav as a new page load. |
| if (navigation_handle->IsSamePage()) |
| return; |
| |
| // Notify other loads that they may have been aborted by this committed load. |
| // Note that by using the committed navigation start as the abort cause, we |
| // lose data on provisional loads that were aborted by other provisional |
| // loads. Those will either be listed as ABORT_OTHER or as being aborted by |
| // this load. |
| NotifyAbortAllLoadsWithTimestamp( |
| AbortTypeForPageTransition(navigation_handle->GetPageTransition()), |
| navigation_handle->NavigationStart()); |
| |
| committed_load_ = std::move(finished_nav); |
| aborted_provisional_loads_.clear(); |
| |
| const GURL& browser_url = web_contents()->GetLastCommittedURL(); |
| const std::string& mime_type = web_contents()->GetContentsMimeType(); |
| DCHECK(!browser_url.is_empty()); |
| DCHECK(!mime_type.empty()); |
| committed_load_->set_renderer_tracked( |
| IsRelevantNavigation(navigation_handle, browser_url, mime_type)); |
| |
| committed_load_->Commit(navigation_handle); |
| } |
| |
| void MetricsWebContentsObserver::NavigationStopped() { |
| NotifyAbortAllLoads(ABORT_STOP); |
| } |
| |
| 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::WasShown() { |
| if (in_foreground_) |
| return; |
| in_foreground_ = true; |
| if (committed_load_) |
| committed_load_->WebContentsShown(); |
| for (const auto& kv : provisional_loads_) { |
| kv.second->WebContentsShown(); |
| } |
| } |
| |
| void MetricsWebContentsObserver::WasHidden() { |
| if (!in_foreground_) |
| return; |
| in_foreground_ = false; |
| if (committed_load_) |
| committed_load_->WebContentsHidden(); |
| for (const auto& kv : provisional_loads_) { |
| kv.second->WebContentsHidden(); |
| } |
| } |
| |
| // 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. Don't notify aborts here because this is probably not user |
| // initiated. If it is (e.g. browser shutdown), other code paths will take care |
| // of notifying. |
| void MetricsWebContentsObserver::RenderProcessGone( |
| 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; |
| } |
| |
| // 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. |
| committed_load_.reset(); |
| aborted_provisional_loads_.clear(); |
| } |
| |
| void MetricsWebContentsObserver::NotifyAbortAllLoads(UserAbortType abort_type) { |
| NotifyAbortAllLoadsWithTimestamp(abort_type, base::TimeTicks::Now()); |
| } |
| |
| void MetricsWebContentsObserver::NotifyAbortAllLoadsWithTimestamp( |
| UserAbortType abort_type, |
| base::TimeTicks timestamp) { |
| if (committed_load_) |
| committed_load_->NotifyAbort(abort_type, timestamp); |
| for (const auto& kv : provisional_loads_) { |
| kv.second->NotifyAbort(abort_type, timestamp); |
| } |
| for (const auto& tracker : aborted_provisional_loads_) { |
| if (tracker->IsLikelyProvisionalAbort(timestamp)) |
| tracker->UpdateAbort(abort_type, timestamp); |
| } |
| aborted_provisional_loads_.clear(); |
| } |
| |
| std::unique_ptr<PageLoadTracker> |
| MetricsWebContentsObserver::NotifyAbortedProvisionalLoadsNewNavigation( |
| content::NavigationHandle* new_navigation) { |
| // 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_.size() == 0) |
| 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->UpdateAbort(ABORT_UNKNOWN_NAVIGATION, timestamp); |
| |
| aborted_provisional_loads_.clear(); |
| return last_aborted_load; |
| } |
| |
| void MetricsWebContentsObserver::OnTimingUpdated( |
| content::RenderFrameHost* render_frame_host, |
| const PageLoadTiming& timing, |
| const PageLoadMetadata& metadata) { |
| bool error = false; |
| if (!committed_load_ || !committed_load_->renderer_tracked()) { |
| RecordInternalError(ERR_IPC_WITH_NO_RELEVANT_LOAD); |
| error = true; |
| } |
| |
| // We may receive notifications from frames that have been navigated away |
| // from. We simply ignore them. |
| if (render_frame_host != web_contents()->GetMainFrame()) { |
| RecordInternalError(ERR_IPC_FROM_WRONG_FRAME); |
| error = true; |
| } |
| |
| // For urls like chrome://newtab, the renderer and browser disagree, |
| // so we have to double check that the renderer isn't sending data from a |
| // bad url like https://www.google.com/_/chrome/newtab. |
| if (!web_contents()->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()) { |
| RecordInternalError(ERR_IPC_FROM_BAD_URL_SCHEME); |
| error = true; |
| } |
| |
| if (error) |
| return; |
| |
| if (!committed_load_->UpdateTiming(timing, metadata)) { |
| // If the page load tracker cannot update its timing, something is wrong |
| // with the IPC (it's from another load, or it's invalid in some other way). |
| // We expect this to be a rare occurrence. |
| RecordInternalError(ERR_BAD_TIMING_IPC); |
| } |
| } |
| |
| } // namespace page_load_metrics |