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