File: navigation_transition_utils.cc

package info (click to toggle)
chromium 120.0.6099.224-1~deb11u1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 6,112,112 kB
  • sloc: cpp: 32,907,025; ansic: 8,148,123; javascript: 3,679,536; python: 2,031,248; asm: 959,718; java: 804,675; xml: 617,256; sh: 111,417; objc: 100,835; perl: 88,443; cs: 53,032; makefile: 29,579; fortran: 24,137; php: 21,162; tcl: 21,147; sql: 20,809; ruby: 17,735; pascal: 12,864; yacc: 8,045; lisp: 3,388; lex: 1,323; ada: 727; awk: 329; jsp: 267; csh: 117; exp: 43; sed: 37
file content (284 lines) | stat: -rw-r--r-- 12,217 bytes parent folder | download
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
// Copyright 2023 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/renderer_host/navigation_transitions/navigation_transition_utils.h"

#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot.h"
#include "content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_cache.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"

namespace content {

namespace {

static gfx::Size g_output_size_for_test = gfx::Size();

static int g_num_copy_requests_issued_for_testing = 0;

void CacheScreenshotImpl(base::WeakPtr<NavigationControllerImpl> controller,
                         int navigation_entry_id,
                         const SkBitmap& bitmap) {
  if (!controller) {
    // The tab was destroyed by the time we receive the bitmap from the GPU.
    return;
  }

  NavigationEntryImpl* entry =
      controller->GetEntryWithUniqueID(navigation_entry_id);
  if (!entry) {
    // The entry was deleted by the time we received the bitmap from the GPU.
    // This can happen by clearing the session history, or when the
    // `NavigationEntry` was replaced or deleted, etc.
    return;
  }

  if (entry == controller->GetLastCommittedEntry()) {
    // TODO(https://crbug.com/1472395): We shouldn't cache the screenshot into
    // the navigation entry if the entry is re-navigated after we send out the
    // copy request. See the two cases below.
    //
    // Consider a fast swipe that triggers history navigation A->B->A, where the
    // second A commits before the GPU responds with the first screenshotting(A)
    // task. Currently `entry == controller->GetLastCommittedEntry()` guards
    // against this stale screenshot; however we should combine with the case
    // below and guard them together (see comments on the crbug).
    //
    // Consider a fast swipe that triggers history navigation A->B->A->B, where
    // the second B commits before the GPU responds with the first
    // screenshotting(A) task. We should discard A's screenshot because it is
    // stale. Currently the capture code does not handle this case. We need to
    // discard the stale screenshot.
    return;
  }

  if (bitmap.drawsNothing()) {
    // The GPU is not able to produce a valid bitmap. This is an error case.
    LOG(ERROR) << "Cannot generate a valid bitmap for entry "
               << entry->GetUniqueID() << " url " << entry->GetURL();
    return;
  }

  SkBitmap immutable_copy(bitmap);
  immutable_copy.setImmutable();

  auto screenshot = std::make_unique<NavigationEntryScreenshot>(
      immutable_copy, entry->GetUniqueID());
  NavigationEntryScreenshotCache* cache =
      controller->GetNavigationEntryScreenshotCache();
  cache->SetScreenshot(entry, std::move(screenshot));
}

void CacheScreenshot(base::WeakPtr<NavigationControllerImpl> controller,
                     int navigation_entry_id,
                     const SkBitmap& bitmap) {
  // `CacheScreenshot`, as the callback for `CopyFromExactSurface`, is not
  // guaranteed to be executed on the same thread that it was submitted from
  // (browser's UI thread). Since `NavigationEntryScreenshotCache` can only be
  // accessed from the browser's UI thread, we explicitly post the caching task
  // onto the UI thread. See https://crbug.com/1217049 for more context.
  GetUIThreadTaskRunner({base::TaskPriority::USER_VISIBLE})
      ->PostTask(FROM_HERE, base::BindOnce(&CacheScreenshotImpl, controller,
                                           navigation_entry_id, bitmap));
}

// We only want to capture screenshots for navigation entries reachable via
// session history navigations. Namely, we don't capture for navigations where
// the previous `NavigationEntry` will be either reloaded or replaced and
// deleted (e.g., `location.replace`, non-primary `FrameTree` navigations, etc).
bool CanTraverseToPreviousEntryAfterNavigation(
    const NavigationRequest& navigation_request) {
  if (navigation_request.GetReloadType() != ReloadType::NONE) {
    // We don't capture for reloads.
    return false;
  }

  if (navigation_request.common_params().should_replace_current_entry) {
    // If the `NavigationEntry` that's about to be committed will replace the
    // previous `NavigationEntry`, we can't traverse to the previous
    // `NavigationEntry` after that.
    // This excludes the first navigation of a tab that replaces the initial
    // `NavigationEntry`, since there is no page to go back to after the initial
    // navigation.
    return false;
  }

  // Navigations in the non-primary `FrameTree` will always replace/reload, as
  // they're guaranteed to only have a single entry for the session history.
  CHECK(navigation_request.frame_tree_node()->frame_tree().is_primary());

  return true;
}

// TODO(liuwilliam): remove it once all the TODOs are implemented.
bool ShouldCaptureForWorkInProgressConditions(
    const NavigationRequest& navigation_request) {
  // TODO(https://crbug.com/1420995): Support same-doc navigations. Make sure
  // to test the `history.pushState` and `history.replaceState` APIs.
  if (navigation_request.IsSameDocument()) {
    return false;
  }

  // TODO(https://crbug.com/1421377): Support subframe navigations.
  if (!navigation_request.IsInMainFrame()) {
    return false;
  }

  if (navigation_request.frame_tree_node()->frame_tree().IsPortal() ||
      navigation_request.frame_tree_node()
          ->GetParentOrOuterDocumentOrEmbedder()) {
    // No support for non-MPArch Portal and GuestView.
    //
    // TODO(https://crbug.com/1422733): We don't need to support capturing
    // Portals, but we should make sure the browser behaves correctly with
    // Portals enabled. When a portal activates, it takes over the session
    // histories from its predecessor. Currently the
    // `NavigationEntryScreenshotCache` and the manager does not support the
    // inheritable screenshots.
    return false;
  }

  // The capture API is currently called from `Navigator::DidNavigate`, which
  // causes early commit navigations to look like same-RFH navigations. These
  // early commit cases currently include navigations from crashed frames and
  // some initial navigations in tabs, neither of which need to have screenshots
  // captured.
  //
  // TODO(https://crbug.com/1473327): We will relocate the capture API callsite
  // into `RenderFrameHostManager::CommitPending`.
  bool is_same_rfh_or_early_commit = navigation_request.GetRenderFrameHost() ==
                                     navigation_request.frame_tree_node()
                                         ->render_manager()
                                         ->current_frame_host();
  if (is_same_rfh_or_early_commit) {
    // TODO(https://crbug.com/1445976): Screenshot capture for same-RFH
    // navigations can yield unexpected results because the
    // `viz::LocalSurfaceId` update is in a different IPC than navigation. We
    // will rely on RenderDocument to be enabled to all navigations.
    return false;
  }

  // TODO(https://crbug.com/1421007): Handle Android native view (e.g. NTP),
  // where the bitmap needs to be generated on the Android side. Move the
  // capture logic into `CaptureNavigationEntryScreenshot`.
  //
  // TODO(https://crbug.com/1474904): Test capturing for WebUI.

  return true;
}

// Purge any existing screenshots from the destination entry. Invalidate instead
// of overwriting here because the screenshot is stale and can't be used
// anymore in future navigations to this entry, as the document that's about to
// be loaded might have different contents than when the screenshot was taken in
// a previous load. A new screenshot should be taken when navigating away from
// this entry again.
void RemoveScreenshotFromDestination(
    const NavigationRequest& navigation_request) {
  NavigationEntry* destination_entry = navigation_request.GetNavigationEntry();

  if (!destination_entry) {
    // We don't always have a destination entry (e.g., a new (non-history)
    // subframe navigation). However if this is a session history navigation, we
    // most-likely have a destination entry to navigate toward, from which we
    // need to purge any existing screenshot.
    return;
  }

  NavigationControllerImpl& nav_controller =
      navigation_request.frame_tree_node()->navigator().controller();

  if (!nav_controller.frame_tree().is_primary()) {
    // Navigations in the non-primary FrameTree can still have a destination
    // entry (e.g., Prerender's initial document-fetch request will create a
    // pending entry), but they won't have a screenshot because the non-primary
    // FrameTree can't access the `NavigationEntryScreenshotCache`.
    CHECK_EQ(nav_controller.GetEntryCount(), 1);
    CHECK(!nav_controller.GetEntryAtIndex(0)->GetUserData(
        NavigationEntryScreenshot::kUserDataKey));
    return;
  }

  NavigationEntryScreenshotCache* cache =
      nav_controller.GetNavigationEntryScreenshotCache();
  if (destination_entry->GetUserData(NavigationEntryScreenshot::kUserDataKey)) {
    std::unique_ptr<NavigationEntryScreenshot> successfully_removed =
        cache->RemoveScreenshot(destination_entry);
    CHECK(successfully_removed);
  }
}
}  // namespace

void NavigationTransitionUtils::SetCapturedScreenshotSizeForTesting(
    const gfx::Size& size) {
  g_output_size_for_test = size;
}

int NavigationTransitionUtils::GetNumCopyOutputRequestIssuedForTesting() {
  return g_num_copy_requests_issued_for_testing;
}

void NavigationTransitionUtils::ResetNumCopyOutputRequestIssuedForTesting() {
  g_num_copy_requests_issued_for_testing = 0;
}

void NavigationTransitionUtils::CaptureNavigationEntryScreenshot(
    const NavigationRequest& navigation_request) {
  if (!AreBackForwardTransitionsEnabled()) {
    return;
  }

  // The current conditions for whether to capture a screenshot depend on
  // `NavigationRequest::GetRenderFrameHost()`, so for now we should only get
  // here after the `RenderFrameHost` has been selected for a successful
  // navigation.
  //
  // TODO(https://crbug.com/1473327): This CHECK won't hold for early-swap. For
  // early-swap, we don't have the network response when we swap the RFHs, thus
  // no RFH on the navigation request. See the comment above
  // `is_same_rfh_or_early_commit`.
  CHECK(navigation_request.HasRenderFrameHost());

  // Remove the screenshot from the destination before checking the conditions.
  // We might not capture for this navigation due to some conditions, but the
  // navigation still continues (to commit/finish), for which we need to remove
  // the screenshot from the destination entry.
  RemoveScreenshotFromDestination(navigation_request);

  if (!CanTraverseToPreviousEntryAfterNavigation(navigation_request)) {
    return;
  }

  // Temporarily check for cases that are not yet supported.
  if (!ShouldCaptureForWorkInProgressConditions(navigation_request)) {
    return;
  }

  //
  // The browser is guaranteed to issue the screenshot request beyond this.
  //

  // Without `SetOutputSizeForTest`, `g_output_size_for_test` is empty, meaning
  // we will capture at full-size, unless specified by tests.
  const gfx::Size output_size = g_output_size_for_test;

  RenderFrameHostImpl* current_rfh =
      navigation_request.frame_tree_node()->current_frame_host();
  RenderWidgetHostView* rwhv = current_rfh->GetView();
  CHECK(rwhv);
  // Make sure the browser is actively embedding a surface.
  CHECK(rwhv->IsSurfaceAvailableForCopy());
  NavigationControllerImpl& nav_controller =
      navigation_request.frame_tree_node()->navigator().controller();
  static_cast<RenderWidgetHostViewBase*>(rwhv)->CopyFromExactSurface(
      /*src_rect=*/gfx::Rect(), output_size,
      base::BindOnce(&CacheScreenshot, nav_controller.GetWeakPtr(),
                     nav_controller.GetLastCommittedEntry()->GetUniqueID()));

  ++g_num_copy_requests_issued_for_testing;
}

}  // namespace content