[go: up one dir, main page]

blob: cc4af1af001dabc15bc339f32fa66e7811ed2067 [file] [log] [blame]
// Copyright 2016 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/page_load_tracker.h"
#include <algorithm>
#include <memory>
#include <ostream>
#include <string>
#include <utility>
#include "base/check_op.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/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_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_handle.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_observer.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
// This macro invokes the specified method on each observer, passing the
// variable length arguments as the method's arguments, and removes the observer
// from the list of observers if the given method returns STOP_OBSERVING.
#define INVOKE_AND_PRUNE_OBSERVERS(observers, Method, ...) \
{ \
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), \
"PageLoadMetricsObserver::" #Method); \
for (auto it = observers.begin(); it != observers.end();) { \
if ((*it)->Method(__VA_ARGS__) == \
PageLoadMetricsObserver::STOP_OBSERVING) { \
it = observers.erase(it); \
} else { \
++it; \
} \
} \
}
namespace page_load_metrics {
namespace internal {
const char kErrorEvents[] = "PageLoad.Internal.ErrorCode";
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";
const char kClientRedirectFirstPaintToNavigation[] =
"PageLoad.Internal.ClientRedirect.FirstPaintToNavigation";
const char kClientRedirectWithoutPaint[] =
"PageLoad.Internal.ClientRedirect.NavigationWithoutPaint";
const char kPageLoadCompletedAfterAppBackground[] =
"PageLoad.Internal.PageLoadCompleted.AfterAppBackground";
const char kPageLoadStartedInForeground[] =
"PageLoad.Internal.NavigationStartedInForeground";
const char kPageLoadPrerender[] = "PageLoad.Internal.Prerender";
const char kPageLoadPrerender2Event[] = "PageLoad.Internal.Prerender2.Event";
} // namespace internal
void RecordInternalError(InternalErrorLoadEvent event) {
UMA_HISTOGRAM_ENUMERATION(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;
return END_OTHER;
}
void LogAbortChainSameURLHistogram(int aborted_chain_size_same_url) {
if (aborted_chain_size_same_url > 0) {
UMA_HISTOGRAM_COUNTS_1M(internal::kAbortChainSizeSameURL,
aborted_chain_size_same_url);
}
}
bool IsNavigationUserInitiated(content::NavigationHandle* handle) {
// TODO(crbug.com/617904): 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/637345): 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 RecordAppBackgroundPageLoadCompleted(bool completed_after_background) {
UMA_HISTOGRAM_BOOLEAN(internal::kPageLoadCompletedAfterAppBackground,
completed_after_background);
}
void DispatchEventsAfterBackForwardCacheRestore(
PageLoadMetricsObserver* 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(PageLoadMetricsObserver* 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);
}
}
} // 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,
int aborted_chain_size,
int aborted_chain_size_same_url)
: did_stop_tracking_(false),
app_entered_background_(false),
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),
aborted_chain_size_(aborted_chain_size),
aborted_chain_size_same_url_(aborted_chain_size_same_url),
embedder_interface_(embedder_interface),
metrics_update_dispatcher_(this, navigation_handle, embedder_interface),
web_contents_(navigation_handle->GetWebContents()),
is_first_navigation_in_web_contents_(
is_first_navigation_in_web_contents) {
DCHECK(!navigation_handle->HasCommitted());
embedder_interface_->RegisterObservers(this);
if (navigation_handle->IsInPrerenderedMainFrame()) {
DCHECK(!started_in_foreground_);
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnPrerenderStart, navigation_handle,
currently_committed_url);
base::UmaHistogramEnumeration(
internal::kPageLoadPrerender2Event,
internal::PageLoadPrerenderEvent::kNavigationInPrerenderedMainFrame);
} else {
source_id_ = ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID);
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnStart, navigation_handle,
currently_committed_url, started_in_foreground_);
}
UMA_HISTOGRAM_BOOLEAN(internal::kPageLoadStartedInForeground,
started_in_foreground_);
if (embedder_interface_->IsNoStatePrefetch(
navigation_handle->GetWebContents()))
UMA_HISTOGRAM_BOOLEAN(internal::kPageLoadPrerender, true);
}
PageLoadTracker::~PageLoadTracker() {
if (app_entered_background_) {
RecordAppBackgroundPageLoadCompleted(true);
}
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);
// Don't include any aborts that resulted in a new navigation, as the chain
// length will be included in the aborter PageLoadTracker.
if (page_end_reason_ != END_RELOAD &&
page_end_reason_ != END_FORWARD_BACK &&
page_end_reason_ != END_NEW_NAVIGATION) {
LogAbortChainHistograms(nullptr);
}
} 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 if (did_commit_) {
observer->OnComplete(metrics_update_dispatcher_.timing());
}
}
}
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_1M(internal::kAbortChainSizeNoCommit,
aborted_chain_size_ + 1);
LogAbortChainSameURLHistogram(aborted_chain_size_same_url_ + 1);
return;
}
// The following is only executed for committing trackers.
DCHECK(did_commit_);
// 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 (EndReasonForPageTransition(committed_transition)) {
case END_RELOAD:
UMA_HISTOGRAM_COUNTS_1M(internal::kAbortChainSizeReload,
aborted_chain_size_);
return;
case END_FORWARD_BACK:
UMA_HISTOGRAM_COUNTS_1M(internal::kAbortChainSizeForwardBack,
aborted_chain_size_);
return;
// TODO(csharrison): Refactor this code so it is based on the WillStart*
// code path instead of the committed load code path. Then, for every abort
// chain, log a histogram of the counts of each of these metrics. For now,
// merge client redirects with new navigations, which was (basically) the
// previous behavior.
case END_CLIENT_REDIRECT:
case END_NEW_NAVIGATION:
UMA_HISTOGRAM_COUNTS_1M(internal::kAbortChainSizeNewNavigation,
aborted_chain_size_);
return;
default:
NOTREACHED()
<< "LogAbortChainHistograms received unexpected ui::PageTransition: "
<< committed_transition;
return;
}
}
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())) {
// Make sure we either started in the foreground and haven't been
// foregrounded yet, or started in the background and have already been
// foregrounded.
base::TimeTicks background_time;
if (!first_background_time_.has_value())
DCHECK_EQ(started_in_foreground_, !first_foreground_time_.has_value());
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();
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnHidden,
metrics_update_dispatcher_.timing());
}
void PageLoadTracker::PageShown() {
// Only log the first time we foreground in a given page load.
if (!first_foreground_time_.has_value()) {
// Make sure we either started in the background and haven't been
// backgrounded yet, or started in the foreground and have already been
// backgrounded.
base::TimeTicks foreground_time;
DCHECK_NE(started_in_foreground_, !first_background_time_.has_value());
foreground_time = base::TimeTicks::Now();
ClampBrowserTimestampIfInterProcessTimeTickSkew(&foreground_time);
DCHECK_GE(foreground_time, navigation_start_);
first_foreground_time_ = foreground_time;
}
visibility_tracker_.OnShown();
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnShown);
}
void PageLoadTracker::SubFrameDeleted(int 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::RenderFrameDeleted(content::RenderFrameHost* rfh) {
for (const auto& observer : observers_) {
observer->OnRenderFrameDeleted(rfh);
}
}
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) {
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());
}
const std::string& mime_type =
navigation_handle->GetWebContents()->GetContentsMimeType();
INVOKE_AND_PRUNE_OBSERVERS(observers_, ShouldObserveMimeType, mime_type);
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnCommit, navigation_handle,
source_id_);
LogAbortChainHistograms(navigation_handle);
}
void PageLoadTracker::DidActivatePrerenderedPage(
content::NavigationHandle* navigation_handle) {
source_id_ = ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID);
if (GetWebContents()->GetVisibility() == content::Visibility::VISIBLE) {
was_prerendered_then_activated_in_foreground_ = true;
PageShown();
}
for (const auto& observer : observers_)
observer->DidActivatePrerenderedPage(navigation_handle);
base::UmaHistogramEnumeration(
internal::kPageLoadPrerender2Event,
internal::PageLoadPrerenderEvent::kPrerenderActivationNavigation);
}
void PageLoadTracker::DidCommitSameDocumentNavigation(
content::NavigationHandle* navigation_handle) {
for (const auto& observer : observers_) {
observer->OnCommitSameDocumentNavigation(navigation_handle);
}
}
void PageLoadTracker::DidInternalNavigationAbort(
content::NavigationHandle* navigation_handle) {
for (const auto& observer : observers_) {
observer->OnDidInternalNavigationAbort(navigation_handle);
}
}
void PageLoadTracker::ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) {
for (const auto& observer : observers_) {
observer->ReadyToCommitNextNavigation(navigation_handle);
}
}
void PageLoadTracker::DidFinishSubFrameNavigation(
content::NavigationHandle* 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_);
failed_provisional_load_info_ = std::make_unique<FailedProvisionalLoadInfo>(
failed_load_time - navigation_handle->NavigationStart(),
navigation_handle->GetNetErrorCode());
}
void PageLoadTracker::Redirect(content::NavigationHandle* navigation_handle) {
url_ = navigation_handle->GetURL();
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnRedirect, navigation_handle);
}
void PageLoadTracker::OnInputEvent(const blink::WebInputEvent& event) {
for (const auto& observer : observers_) {
observer->OnUserInput(event, metrics_update_dispatcher_.timing());
}
}
void PageLoadTracker::FlushMetricsOnAppEnterBackground() {
metrics_update_dispatcher()->FlushPendingTimingUpdates();
if (!app_entered_background_) {
RecordAppBackgroundPageLoadCompleted(false);
app_entered_background_ = true;
}
INVOKE_AND_PRUNE_OBSERVERS(observers_, FlushMetricsOnAppEnterBackground,
metrics_update_dispatcher_.timing());
}
void PageLoadTracker::NotifyClientRedirectTo(
content::NavigationHandle* destination) {
if (metrics_update_dispatcher_.timing().paint_timing->first_paint) {
base::TimeTicks first_paint_time =
navigation_start() +
metrics_update_dispatcher_.timing().paint_timing->first_paint.value();
base::TimeDelta first_paint_to_navigation;
if (destination->NavigationStart() > first_paint_time)
first_paint_to_navigation =
destination->NavigationStart() - first_paint_time;
PAGE_LOAD_HISTOGRAM(internal::kClientRedirectFirstPaintToNavigation,
first_paint_to_navigation);
} else {
UMA_HISTOGRAM_BOOLEAN(internal::kClientRedirectWithoutPaint, true);
}
}
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,
const net::CookieList& cookie_list,
bool blocked_by_policy) {
for (const auto& observer : observers_) {
observer->OnCookiesRead(url, first_party_url, cookie_list,
blocked_by_policy);
}
}
void PageLoadTracker::OnCookieChange(const GURL& url,
const GURL& first_party_url,
const net::CanonicalCookie& cookie,
bool blocked_by_policy) {
for (const auto& observer : observers_) {
observer->OnCookieChange(url, first_party_url, cookie, blocked_by_policy);
}
}
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_.clear();
}
void PageLoadTracker::AddObserver(
std::unique_ptr<PageLoadMetricsObserver> observer) {
observer->SetDelegate(this);
observers_.push_back(std::move(observer));
}
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;
}
bool PageLoadTracker::MatchesOriginalNavigation(
content::NavigationHandle* navigation_handle) {
// Neither navigation should have committed.
DCHECK(!navigation_handle->HasCommitted());
DCHECK(!did_commit_);
return navigation_handle->GetURL() == start_url_;
}
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 1
// second after a user interaction are considered to be triggered by user
// activation (based on HTML spec:
// https://html.spec.whatwg.org/multipage/interaction.html#triggered-by-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_);
}
}
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);
}
void PageLoadTracker::OnTimingChanged() {
DCHECK(!last_dispatched_merged_page_timing_->Equals(
metrics_update_dispatcher_.timing()));
const mojom::PaintTimingPtr& paint_timing =
metrics_update_dispatcher_.timing().paint_timing;
largest_contentful_paint_handler_.RecordTiming(
*paint_timing->largest_contentful_paint,
paint_timing->first_input_or_scroll_notified_timestamp,
nullptr /* subframe_rfh */);
experimental_largest_contentful_paint_handler_.RecordTiming(
*paint_timing->experimental_largest_contentful_paint,
paint_timing->first_input_or_scroll_notified_timestamp,
nullptr /* subframe_rfh */);
for (const auto& observer : observers_) {
DispatchObserverTimingCallbacks(observer.get(),
*last_dispatched_merged_page_timing_,
metrics_update_dispatcher_.timing());
}
last_dispatched_merged_page_timing_ =
metrics_update_dispatcher_.timing().Clone();
}
void PageLoadTracker::OnSubFrameTimingChanged(
content::RenderFrameHost* rfh,
const mojom::PageLoadTiming& timing) {
DCHECK(rfh->GetParent());
const mojom::PaintTimingPtr& paint_timing = timing.paint_timing;
largest_contentful_paint_handler_.RecordTiming(
*paint_timing->largest_contentful_paint,
paint_timing->first_input_or_scroll_notified_timestamp, rfh);
experimental_largest_contentful_paint_handler_.RecordTiming(
*paint_timing->experimental_largest_contentful_paint,
paint_timing->first_input_or_scroll_notified_timestamp, rfh);
for (const auto& observer : observers_) {
observer->OnTimingUpdate(rfh, timing);
}
}
void PageLoadTracker::OnSubFrameInputTimingChanged(
content::RenderFrameHost* rfh,
const mojom::InputTiming& input_timing_delta) {
DCHECK(rfh->GetParent());
for (const auto& observer : observers_) {
observer->OnInputTimingUpdate(rfh, input_timing_delta);
}
}
void PageLoadTracker::OnSubFrameRenderDataChanged(
content::RenderFrameHost* rfh,
const mojom::FrameRenderDataUpdate& render_data) {
DCHECK(rfh->GetParent());
for (const auto& observer : observers_) {
observer->OnSubFrameRenderDataUpdate(rfh, render_data);
}
}
void PageLoadTracker::OnMainFrameMetadataChanged() {
for (const auto& observer : observers_) {
observer->OnLoadingBehaviorObserved(nullptr,
GetMainFrameMetadata().behavior_flags);
}
}
void PageLoadTracker::OnSubframeMetadataChanged(
content::RenderFrameHost* rfh,
const mojom::FrameMetadata& metadata) {
for (const auto& observer : observers_) {
observer->OnLoadingBehaviorObserved(rfh, metadata.behavior_flags);
}
}
void PageLoadTracker::OnSubFrameMobileFriendlinessChanged(
const blink::MobileFriendliness& mobile_friendliness) {
for (const auto& observer : observers_) {
observer->OnMobileFriendlinessUpdate(mobile_friendliness);
}
}
void PageLoadTracker::OnPrefetchLikely() {
for (const auto& observer : observers_) {
observer->OnPrefetchLikely();
}
}
void PageLoadTracker::DidActivatePortal(base::TimeTicks activation_time) {
for (const auto& observer : observers_)
observer->DidActivatePortal(activation_time);
}
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::SetUpSharedMemoryForSmoothness(
base::ReadOnlySharedMemoryRegion shared_memory) {
DCHECK(shared_memory.IsValid());
for (auto& observer : observers_) {
observer->SetUpSharedMemoryForSmoothness(shared_memory);
}
}
void PageLoadTracker::UpdateResourceDataUse(
content::RenderFrameHost* rfh,
const std::vector<mojom::ResourceDataUpdatePtr>& resources) {
resource_tracker_.UpdateResourceDataUse(rfh->GetProcess()->GetID(),
resources);
for (const auto& observer : observers_) {
observer->OnResourceDataUseObserved(rfh, resources);
}
}
void PageLoadTracker::OnNewDeferredResourceCounts(
const mojom::DeferredResourceCounts& new_deferred_resource_data) {
for (const auto& observer : observers_) {
observer->OnNewDeferredResourceCounts(new_deferred_resource_data);
}
}
void PageLoadTracker::UpdateFrameCpuTiming(content::RenderFrameHost* rfh,
const mojom::CpuTiming& timing) {
for (const auto& observer : observers_) {
observer->OnCpuTimingUpdate(rfh, timing);
}
}
void PageLoadTracker::OnFrameIntersectionUpdate(
content::RenderFrameHost* rfh,
const mojom::FrameIntersectionUpdate& frame_intersection_update) {
for (const auto& observer : observers_) {
observer->OnFrameIntersectionUpdate(rfh, frame_intersection_update);
}
}
content::WebContents* PageLoadTracker::GetWebContents() const {
return web_contents_;
}
base::TimeTicks PageLoadTracker::GetNavigationStart() const {
return navigation_start_;
}
absl::optional<base::TimeDelta>
PageLoadTracker::DurationSinceNavigationStartForTime(
const absl::optional<base::TimeTicks>& time) const {
absl::optional<base::TimeDelta> duration;
if (!time.has_value())
return duration;
DCHECK_GE(time.value(), navigation_start_);
duration = time.value() - navigation_start_;
return duration;
}
absl::optional<base::TimeDelta> PageLoadTracker::GetTimeToFirstBackground()
const {
return DurationSinceNavigationStartForTime(first_background_time_);
}
absl::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_;
}
bool PageLoadTracker::WasPrerenderedThenActivatedInForeground() const {
return was_prerendered_then_activated_in_foreground_;
}
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_;
}
absl::optional<base::TimeDelta> PageLoadTracker::GetTimeToPageEnd() const {
if (page_end_reason_ != END_NONE) {
return DurationSinceNavigationStartForTime(page_end_time_);
}
DCHECK(page_end_time_.is_null());
return absl::optional<base::TimeDelta>();
}
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 NormalizedResponsivenessMetrics&
PageLoadTracker::GetNormalizedResponsivenessMetrics() const {
return metrics_update_dispatcher_.normalized_responsiveness_metrics();
}
const mojom::InputTiming& PageLoadTracker::GetPageInputTiming() const {
return metrics_update_dispatcher_.page_input_timing();
}
const blink::MobileFriendliness& PageLoadTracker::GetMobileFriendliness()
const {
return metrics_update_dispatcher_.mobile_friendliness();
}
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 {
return source_id_;
}
bool PageLoadTracker::IsFirstNavigationInWebContents() const {
return is_first_navigation_in_web_contents_;
}
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.
INVOKE_AND_PRUNE_OBSERVERS(observers_, OnEnterBackForwardCache,
metrics_update_dispatcher_.timing());
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);
}
} // namespace page_load_metrics