File: fenced_frame_viewport_observer.cc

package info (click to toggle)
chromium 140.0.7339.185-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,193,740 kB
  • sloc: cpp: 35,093,945; ansic: 7,161,670; javascript: 4,199,694; python: 1,441,797; asm: 949,904; xml: 747,515; pascal: 187,748; perl: 88,691; sh: 88,248; objc: 79,953; sql: 52,714; cs: 44,599; fortran: 24,137; makefile: 22,114; tcl: 15,277; php: 13,980; yacc: 9,000; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (316 lines) | stat: -rw-r--r-- 12,680 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// 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