blob: f479e201ede91402a0d160ccfaa842a79a4fe5d5 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/fenced_frame/fenced_frame_viewport_observer.h"
#include <map>
#include <utility>
#include "base/metrics/histogram_functions.h"
#include "base/numerics/checked_math.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/system_entropy_utils.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/frame_tree_node_id.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/web_contents.h"
#include "net/base/schemeful_site.h"
#include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
#include "third_party/blink/public/common/navigation/navigation_params.h"
#include "url/origin.h"
namespace content {
namespace {
using FrameVisibility = blink::mojom::FrameVisibility;
net::SchemefulSite GetSchemefulSiteFromPossiblyOpaqueOrigin(
const url::Origin& origin) {
const url::SchemeHostPort& scheme_host_port =
origin.GetTupleOrPrecursorTupleIfOpaque();
if (!scheme_host_port.IsValid()) {
return net::SchemefulSite();
}
return net::SchemefulSite(url::Origin::CreateFromNormalizedTuple(
scheme_host_port.scheme(), scheme_host_port.host(),
scheme_host_port.port()));
}
// If this navigation will result in an error page, or it already has, then
// we need to track the pre-navigation site of the frame. If a fenced frame
// attempts to navigate itself to an error page on purpose, we'd need to track
// the site that was in the frame before the error page (which has an
// opaque origin, so we can't construct its net::SchemefulSite directly).
// We're choosing the pre-navigation site instead of the initiator site because
// the initiator might be the fenced frame's embedder, rather than the document
// that is already in the frame.
bool ShouldTrackPreviousSiteForNavigation(NavigationHandle* navigation_handle) {
if (navigation_handle->GetNetErrorCode() != net::Error::OK) {
return true;
}
// For extra safety, check the error page status of committed navigations.
if (navigation_handle->HasCommitted() && navigation_handle->IsErrorPage()) {
return true;
}
return false;
}
// Determines the site that a frame will be tracked under for viewport metrics.
//
// IMPORTANT: This will not always return the same site that the navigation
// intends to commit or has committed. In cases like error pages we need to take
// special precautions to ensure the site that was previously committed in the
// frame was tracked instead.
net::SchemefulSite GetSiteForViewportMetricsTracking(
const FencedFrameViewportMonitor::FencedFrameVisibilityInfo& info,
NavigationHandle* navigation_handle) {
if (ShouldTrackPreviousSiteForNavigation(navigation_handle)) {
const url::Origin& last_successful_origin =
static_cast<NavigationRequest*>(navigation_handle)
->frame_tree_node()
->last_successful_origin();
return GetSchemefulSiteFromPossiblyOpaqueOrigin(last_successful_origin);
}
const url::Origin& origin_to_track =
navigation_handle->GetRenderFrameHost()->GetLastCommittedOrigin();
// If the origin whose site we're planning to track is opaque, then we want
// the site of the precursor origin instead, to determine the actual eTLD+1
// that the navigation was intended for, independent of the origin's
// actual opaqueness. The origin may not have a precursor, which will result
// in returning an opaque SchemefulSite. This isn't ideal, but it's the only
// option if we don't have enough information to proceed.
return GetSchemefulSiteFromPossiblyOpaqueOrigin(origin_to_track);
}
} // namespace
FencedFrameViewportObserver::FencedFrameViewportObserver(
WebContents* web_contents)
: WebContentsObserver(web_contents) {}
FencedFrameViewportObserver::~FencedFrameViewportObserver() = default;
void FencedFrameViewportObserver::FrameDeleted(
FrameTreeNodeId frame_tree_node_id) {
// This is sketchy but safe to do, as the FrameTreeNode being destroyed still
// technically exists at this point.
FrameTreeNode* node = FrameTreeNode::GloballyFindByID(frame_tree_node_id);
if (!node || !node->IsFencedFrameRoot()) {
return;
}
auto* monitor = PageUserData<FencedFrameViewportMonitor>::GetOrCreateForPage(
node->GetParentOrOuterDocument()->GetOutermostMainFrame()->GetPage());
if (monitor) {
monitor->FrameDeleted(frame_tree_node_id);
}
}
void FencedFrameViewportObserver::DidFinishNavigation(
NavigationHandle* navigation_handle) {
if (!(navigation_handle->IsInFencedFrameTree() &&
navigation_handle->IsInMainFrame())) {
return;
}
// If the navigation never committed (download, HTTP 204, etc), then there's
// no site information to update. Same goes for same-document navigations.
// Error page commits are allowed to proceed.
if (!navigation_handle->HasCommitted() ||
navigation_handle->IsSameDocument()) {
return;
}
auto* monitor = PageUserData<FencedFrameViewportMonitor>::GetOrCreateForPage(
navigation_handle->GetRenderFrameHost()
->GetOutermostMainFrame()
->GetPage());
if (monitor) {
monitor->DidFinishNavigation(navigation_handle);
}
}
void FencedFrameViewportObserver::OnFrameVisibilityChanged(
RenderFrameHost* rfh,
FrameVisibility visibility) {
if (!rfh->IsFencedFrameRoot()) {
return;
}
auto* monitor = PageUserData<FencedFrameViewportMonitor>::GetOrCreateForPage(
rfh->GetOutermostMainFrame()->GetPage());
if (monitor) {
auto* rfhi = static_cast<RenderFrameHostImpl*>(rfh);
monitor->OnFrameVisibilityChanged(rfhi->GetFrameTreeNodeId(), visibility);
}
}
PAGE_USER_DATA_KEY_IMPL(FencedFrameViewportMonitor);
// Ideally, we'd have a CHECK in place here that we only construct this object
// for the primary page. However, there are cases where the primary page isn't
// actually qualified as such yet, like prerendering (see Page::IsPrimary() for
// more info). For now, rely on the caller providing the page associated with
// the outermost main frame, which should result in the desired behavior.
FencedFrameViewportMonitor::FencedFrameViewportMonitor(Page& page)
: PageUserData<FencedFrameViewportMonitor>(page) {}
FencedFrameViewportMonitor::~FencedFrameViewportMonitor() {
LogUmaMetrics();
}
void FencedFrameViewportMonitor::FrameDeleted(
FrameTreeNodeId frame_tree_node_id) {
auto iter = visibility_infos_.find(frame_tree_node_id);
if (iter == visibility_infos_.end()) {
return;
}
// If this frame was in the viewport, then try to decrement the count for the
// current site.
if (iter->second.current_visibility == FrameVisibility::kRenderedInViewport) {
DecrementFencedFrameViewportCountForSite(iter->second.site);
}
visibility_infos_.erase(iter);
}
void FencedFrameViewportMonitor::DidFinishNavigation(
NavigationHandle* navigation_handle) {
// Get the FencedFrameVisibilityInfo for this frame, or default-construct one
// if it doesn't exist, which may happen if the frame hasn't navigated or
// entered the viewport yet.
auto& info = visibility_infos_[navigation_handle->GetFrameTreeNodeId()];
net::SchemefulSite site_to_track =
GetSiteForViewportMetricsTracking(info, navigation_handle);
// The frame has navigated to a new site while in the viewport. We need to
// update the counts for both the old and new site. If the frame has navigated
// while outside the viewport, or the navigation is same-site, there's no
// new metrics to log.
if (info.current_visibility == FrameVisibility::kRenderedInViewport &&
site_to_track != info.site) {
IncrementFencedFrameViewportCountForSite(site_to_track);
DecrementFencedFrameViewportCountForSite(info.site);
}
info.site = site_to_track;
}
void FencedFrameViewportMonitor::OnFrameVisibilityChanged(
FrameTreeNodeId frame_tree_node_id,
FrameVisibility visibility) {
// Get the FencedFrameVisibilityInfo for this frame, or default-construct one
// if it doesn't exist, which may happen if the frame hasn't navigated or
// entered the viewport yet.
auto& info = visibility_infos_[frame_tree_node_id];
// If the frame is entering the viewport, increment the count for the last
// successful site. If the frame is leaving the viewport, decrement the
// count instead.
if (info.current_visibility != FrameVisibility::kRenderedInViewport &&
visibility == FrameVisibility::kRenderedInViewport) {
IncrementFencedFrameViewportCountForSite(info.site);
} else if (info.current_visibility == FrameVisibility::kRenderedInViewport &&
visibility != FrameVisibility::kRenderedInViewport) {
DecrementFencedFrameViewportCountForSite(info.site);
}
info.current_visibility = visibility;
}
void FencedFrameViewportMonitor::
ComputeSameSiteFencedFrameMaximumBeforePrimaryPageChange() {
if (has_computed_unload_count_) {
return;
}
auto max_iter =
std::max_element(fenced_frames_in_viewport_per_site_.begin(),
fenced_frames_in_viewport_per_site_.end(),
[](const std::pair<net::SchemefulSite, int>& a,
const std::pair<net::SchemefulSite, int>& b) {
return a.second < b.second;
});
if (max_iter != fenced_frames_in_viewport_per_site_.end()) {
max_same_site_fenced_frames_in_viewport_at_unload_count_ = max_iter->second;
}
has_computed_unload_count_ = true;
}
void FencedFrameViewportMonitor::OnPrimaryPageEnteringBFCache() {
// Normally, we'd log UMA metrics when this object is destroyed, such as when
// the primary main frame changes after a navigation, or the WebContents is
// torn down. However, when a Page enters BackForwardCache, it's not
// destroyed, even though the primary main frame is changing. So, we need to
// log UMA metrics now.
LogUmaMetrics();
// Now that we've logged the previous round of metrics, we need to initialize
// the counters to the correct values for when this page is restored from
// BackForwardCache. When the page is restored, we'll already know the max
// number of same-site fenced frames in the viewport: it's the same as when
// the page entered BackForwardCache in the first place! NOTE: We don't need
// to re-initialize any of the per-frame data structures here, because no
// notifications fire that update individual frame state when entering or
// leaving BackForwardCache.
max_same_site_fenced_frames_in_viewport_count_ =
max_same_site_fenced_frames_in_viewport_at_unload_count_;
// Also, allow the unload count to be computed again when the restored page
// eventually unloads or re-enters BackForwardCache.
has_computed_unload_count_ = false;
}
void FencedFrameViewportMonitor::LogUmaMetrics() {
// If we're not tracking any fenced frames, there's no reason to log any
// metrics. We also log metrics before the Page enters BackForwardCache, so no
// need to log again if we've already entered.
if (fenced_frames_in_viewport_per_site_.empty() ||
static_cast<RenderFrameHostImpl*>(&(page().GetMainDocument()))
->IsInBackForwardCache()) {
return;
}
base::UmaHistogramExactLinear(
blink::kMaxSameSiteFencedFramesInViewportPerPageLoad,
max_same_site_fenced_frames_in_viewport_count_, 101);
base::UmaHistogramExactLinear(
blink::kMaxSameSiteFencedFramesInViewportAtUnload,
max_same_site_fenced_frames_in_viewport_at_unload_count_, 101);
}
void FencedFrameViewportMonitor::IncrementFencedFrameViewportCountForSite(
const net::SchemefulSite& site) {
// If this line performs an insertion, site_count should be zero-initialized.
int& site_count = fenced_frames_in_viewport_per_site_[site];
site_count += 1;
// If a site's count increases, check if it exceeds the max same-site count
// we've seen for this primary page load.
if (site_count > max_same_site_fenced_frames_in_viewport_count_) {
max_same_site_fenced_frames_in_viewport_count_ = site_count;
}
}
void FencedFrameViewportMonitor::DecrementFencedFrameViewportCountForSite(
const net::SchemefulSite& site) {
// If this line performs an insertion, site_count should be zero-initialized.
int& site_count = fenced_frames_in_viewport_per_site_[site];
CHECK_GE(site_count, 0);
if (site_count > 0) {
site_count -= 1;
}
}
} // namespace content