1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/page_load_metrics/browser/metrics_web_contents_observer.h"
6
7 #include <algorithm>
8 #include <ostream>
9 #include <string>
10 #include <utility>
11
12 #include "base/location.h"
13 #include "base/memory/ptr_util.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "components/page_load_metrics/browser/page_load_metrics_embedder_interface.h"
16 #include "components/page_load_metrics/browser/page_load_metrics_update_dispatcher.h"
17 #include "components/page_load_metrics/browser/page_load_metrics_util.h"
18 #include "components/page_load_metrics/browser/page_load_tracker.h"
19 #include "components/page_load_metrics/common/page_load_timing.h"
20 #include "content/public/browser/back_forward_cache.h"
21 #include "content/public/browser/browser_thread.h"
22 #include "content/public/browser/cookie_access_details.h"
23 #include "content/public/browser/global_request_id.h"
24 #include "content/public/browser/global_routing_id.h"
25 #include "content/public/browser/media_player_id.h"
26 #include "content/public/browser/navigation_details.h"
27 #include "content/public/browser/navigation_handle.h"
28 #include "content/public/browser/render_frame_host.h"
29 #include "content/public/browser/render_process_host.h"
30 #include "content/public/browser/render_view_host.h"
31 #include "content/public/browser/web_contents.h"
32 #include "content/public/browser/web_contents_observer.h"
33 #include "content/public/browser/web_contents_user_data.h"
34 #include "net/base/ip_endpoint.h"
35 #include "net/base/net_errors.h"
36 #include "services/network/public/mojom/fetch_api.mojom-shared.h"
37 #include "third_party/blink/public/common/loader/resource_type_util.h"
38 #include "third_party/blink/public/common/mobile_metrics/mobile_friendliness.h"
39 #include "ui/base/page_transition_types.h"
40
41 namespace page_load_metrics {
42
43 namespace {
44
45 // Returns the HTTP status code for the current page, or -1 if no status code
46 // is available. Can only be called if the |navigation_handle| has committed.
GetHttpStatusCode(content::NavigationHandle * navigation_handle)47 int GetHttpStatusCode(content::NavigationHandle* navigation_handle) {
48 DCHECK(navigation_handle->HasCommitted());
49 const net::HttpResponseHeaders* response_headers =
50 navigation_handle->GetResponseHeaders();
51 if (!response_headers)
52 return -1;
53 return response_headers->response_code();
54 }
55
CreateUserInitiatedInfo(content::NavigationHandle * navigation_handle)56 UserInitiatedInfo CreateUserInitiatedInfo(
57 content::NavigationHandle* navigation_handle) {
58 if (!navigation_handle->IsRendererInitiated())
59 return UserInitiatedInfo::BrowserInitiated();
60
61 return UserInitiatedInfo::RenderInitiated(
62 navigation_handle->HasUserGesture(),
63 !navigation_handle->NavigationInputStart().is_null());
64 }
65
66 } // namespace
67
68 // static
RecordFeatureUsage(content::RenderFrameHost * render_frame_host,const mojom::PageLoadFeatures & new_features)69 void MetricsWebContentsObserver::RecordFeatureUsage(
70 content::RenderFrameHost* render_frame_host,
71 const mojom::PageLoadFeatures& new_features) {
72 content::WebContents* web_contents =
73 content::WebContents::FromRenderFrameHost(render_frame_host);
74 MetricsWebContentsObserver* observer =
75 MetricsWebContentsObserver::FromWebContents(web_contents);
76 if (observer)
77 observer->OnBrowserFeatureUsage(render_frame_host, new_features);
78 }
79
80 // static
CreateForWebContents(content::WebContents * web_contents,std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface)81 MetricsWebContentsObserver* MetricsWebContentsObserver::CreateForWebContents(
82 content::WebContents* web_contents,
83 std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface) {
84 DCHECK(web_contents);
85
86 MetricsWebContentsObserver* metrics = FromWebContents(web_contents);
87 if (!metrics) {
88 metrics = new MetricsWebContentsObserver(web_contents,
89 std::move(embedder_interface));
90 web_contents->SetUserData(UserDataKey(), base::WrapUnique(metrics));
91 }
92 return metrics;
93 }
94
~MetricsWebContentsObserver()95 MetricsWebContentsObserver::~MetricsWebContentsObserver() {}
96
WebContentsWillSoonBeDestroyed()97 void MetricsWebContentsObserver::WebContentsWillSoonBeDestroyed() {
98 web_contents_will_soon_be_destroyed_ = true;
99 }
100
WebContentsDestroyed()101 void MetricsWebContentsObserver::WebContentsDestroyed() {
102 // TODO(csharrison): Use a more user-initiated signal for CLOSE.
103 NotifyPageEndAllLoads(END_CLOSE, UserInitiatedInfo::NotUserInitiated());
104
105 // We tear down PageLoadTrackers in WebContentsDestroyed, rather than in the
106 // destructor, since |web_contents()| returns nullptr in the destructor, and
107 // PageLoadMetricsObservers can cause code to execute that wants to be able to
108 // access the current WebContents.
109 committed_load_ = nullptr;
110 ukm_smoothness_data_ = {};
111 provisional_loads_.clear();
112 aborted_provisional_loads_.clear();
113
114 for (auto& observer : testing_observers_)
115 observer.OnGoingAway();
116 }
117
RegisterInputEventObserver(content::RenderViewHost * host)118 void MetricsWebContentsObserver::RegisterInputEventObserver(
119 content::RenderViewHost* host) {
120 if (host != nullptr)
121 host->GetWidget()->AddInputEventObserver(this);
122 }
123
UnregisterInputEventObserver(content::RenderViewHost * host)124 void MetricsWebContentsObserver::UnregisterInputEventObserver(
125 content::RenderViewHost* host) {
126 if (host != nullptr)
127 host->GetWidget()->RemoveInputEventObserver(this);
128 }
129
RenderViewHostChanged(content::RenderViewHost * old_host,content::RenderViewHost * new_host)130 void MetricsWebContentsObserver::RenderViewHostChanged(
131 content::RenderViewHost* old_host,
132 content::RenderViewHost* new_host) {
133 UnregisterInputEventObserver(old_host);
134 RegisterInputEventObserver(new_host);
135 }
136
FrameDeleted(content::RenderFrameHost * rfh)137 void MetricsWebContentsObserver::FrameDeleted(content::RenderFrameHost* rfh) {
138 if (committed_load_)
139 committed_load_->FrameDeleted(rfh);
140 }
141
RenderFrameDeleted(content::RenderFrameHost * rfh)142 void MetricsWebContentsObserver::RenderFrameDeleted(
143 content::RenderFrameHost* rfh) {
144 // PageLoadTracker can be associated only with a main frame.
145 if (rfh->GetParent())
146 return;
147 back_forward_cached_pages_.erase(rfh);
148 }
149
MediaStartedPlaying(const content::WebContentsObserver::MediaPlayerInfo & video_type,const content::MediaPlayerId & id)150 void MetricsWebContentsObserver::MediaStartedPlaying(
151 const content::WebContentsObserver::MediaPlayerInfo& video_type,
152 const content::MediaPlayerId& id) {
153 if (!id.render_frame_host->GetMainFrame()->IsCurrent()) {
154 // Ignore media that starts playing in a page that was navigated away
155 // from.
156 return;
157 }
158
159 if (committed_load_)
160 committed_load_->MediaStartedPlaying(video_type, id.render_frame_host);
161 }
162
WillStartNavigationRequest(content::NavigationHandle * navigation_handle)163 void MetricsWebContentsObserver::WillStartNavigationRequest(
164 content::NavigationHandle* navigation_handle) {
165 // Same-document navigations should never go through
166 // WillStartNavigationRequest.
167 DCHECK(!navigation_handle->IsSameDocument());
168
169 if (!navigation_handle->IsInMainFrame())
170 return;
171
172 WillStartNavigationRequestImpl(navigation_handle);
173 has_navigated_ = true;
174 }
175
MetricsWebContentsObserver(content::WebContents * web_contents,std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface)176 MetricsWebContentsObserver::MetricsWebContentsObserver(
177 content::WebContents* web_contents,
178 std::unique_ptr<PageLoadMetricsEmbedderInterface> embedder_interface)
179 : content::WebContentsObserver(web_contents),
180 in_foreground_(web_contents->GetVisibility() !=
181 content::Visibility::HIDDEN),
182 embedder_interface_(std::move(embedder_interface)),
183 has_navigated_(false),
184 page_load_metrics_receiver_(web_contents, this) {
185 // Prerenders erroneously report that they are initially visible, so we
186 // manually override visibility state for prerender.
187 if (embedder_interface_->IsPrerender(web_contents))
188 in_foreground_ = false;
189
190 RegisterInputEventObserver(web_contents->GetRenderViewHost());
191 }
192
WillStartNavigationRequestImpl(content::NavigationHandle * navigation_handle)193 void MetricsWebContentsObserver::WillStartNavigationRequestImpl(
194 content::NavigationHandle* navigation_handle) {
195 UserInitiatedInfo user_initiated_info(
196 CreateUserInitiatedInfo(navigation_handle));
197 std::unique_ptr<PageLoadTracker> last_aborted =
198 NotifyAbortedProvisionalLoadsNewNavigation(navigation_handle,
199 user_initiated_info);
200
201 int chain_size_same_url = 0;
202 int chain_size = 0;
203 if (last_aborted) {
204 if (last_aborted->MatchesOriginalNavigation(navigation_handle)) {
205 chain_size_same_url = last_aborted->aborted_chain_size_same_url() + 1;
206 } else if (last_aborted->aborted_chain_size_same_url() > 0) {
207 LogAbortChainSameURLHistogram(
208 last_aborted->aborted_chain_size_same_url());
209 }
210 chain_size = last_aborted->aborted_chain_size() + 1;
211 }
212
213 if (!ShouldTrackMainFrameNavigation(navigation_handle))
214 return;
215
216 // Pass in the last committed url to the PageLoadTracker. If the MWCO has
217 // never observed a committed load, use the last committed url from this
218 // WebContent's opener. This is more accurate than using referrers due to
219 // referrer sanitizing and origin referrers. Note that this could potentially
220 // be inaccurate if the opener has since navigated.
221 content::RenderFrameHost* opener = web_contents()->GetOpener();
222 const GURL& opener_url = !has_navigated_ && opener
223 ? opener->GetLastCommittedURL()
224 : GURL::EmptyGURL();
225 const GURL& currently_committed_url =
226 committed_load_ ? committed_load_->url() : opener_url;
227
228 // Passing raw pointers to |observers_| and |embedder_interface_| is safe
229 // because the MetricsWebContentsObserver owns them both list and they are
230 // torn down after the PageLoadTracker. The PageLoadTracker does not hold on
231 // to |committed_load_| or |navigation_handle| beyond the scope of the
232 // constructor.
233 auto insertion_result = provisional_loads_.insert(std::make_pair(
234 navigation_handle,
235 std::make_unique<PageLoadTracker>(
236 in_foreground_, embedder_interface_.get(), currently_committed_url,
237 !has_navigated_, navigation_handle, user_initiated_info, chain_size,
238 chain_size_same_url)));
239 DCHECK(insertion_result.second)
240 << "provisional_loads_ already contains NavigationHandle.";
241 for (auto& observer : testing_observers_)
242 observer.OnTrackerCreated(insertion_result.first->second.get());
243 }
244
WillProcessNavigationResponse(content::NavigationHandle * navigation_handle)245 void MetricsWebContentsObserver::WillProcessNavigationResponse(
246 content::NavigationHandle* navigation_handle) {
247 auto it = provisional_loads_.find(navigation_handle);
248 if (it == provisional_loads_.end())
249 return;
250 it->second->WillProcessNavigationResponse(navigation_handle);
251 }
252
GetTrackerOrNullForRequest(const content::GlobalRequestID & request_id,content::RenderFrameHost * render_frame_host_or_null,network::mojom::RequestDestination request_destination,base::TimeTicks creation_time)253 PageLoadTracker* MetricsWebContentsObserver::GetTrackerOrNullForRequest(
254 const content::GlobalRequestID& request_id,
255 content::RenderFrameHost* render_frame_host_or_null,
256 network::mojom::RequestDestination request_destination,
257 base::TimeTicks creation_time) {
258 if (request_destination == network::mojom::RequestDestination::kDocument) {
259 DCHECK(request_id != content::GlobalRequestID());
260 // The main frame request can complete either before or after commit, so we
261 // look at both provisional loads and the committed load to find a
262 // PageLoadTracker with a matching request id. See https://goo.gl/6TzCYN for
263 // more details.
264 for (const auto& kv : provisional_loads_) {
265 PageLoadTracker* candidate = kv.second.get();
266 if (candidate->HasMatchingNavigationRequestID(request_id)) {
267 return candidate;
268 }
269 }
270 if (committed_load_ &&
271 committed_load_->HasMatchingNavigationRequestID(request_id)) {
272 return committed_load_.get();
273 }
274 } else {
275 // Non main frame resources are always associated with the currently
276 // committed load. If the resource request was started before this
277 // navigation then it should be ignored.
278 if (!committed_load_ || creation_time < committed_load_->navigation_start())
279 return nullptr;
280
281 // Sub-frame resources have a null RFH when browser-side navigation is
282 // enabled, so we can't perform the RFH check below for them.
283 //
284 // TODO(bmcquade): consider tracking GlobalRequestIDs for sub-frame
285 // navigations in each PageLoadTracker, and performing a lookup for
286 // sub-frames similar to the main-frame lookup above.
287 if (blink::IsRequestDestinationFrame(request_destination))
288 return committed_load_.get();
289
290 // This was originally a DCHECK but it fails when the document load happened
291 // after client certificate selection.
292 if (!render_frame_host_or_null)
293 return nullptr;
294
295 // There is a race here: a completed resource for the previously committed
296 // page can arrive after the new page has committed. In this case, we may
297 // attribute the resource to the wrong page load. We do our best to guard
298 // against this by verifying that the RFH for the resource matches the RFH
299 // for the currently committed load, however there are cases where the same
300 // RFH is used across page loads (same origin navigations, as well as some
301 // cross-origin render-initiated navigations).
302 //
303 // TODO(crbug.com/738577): use a DocumentId here instead, to eliminate this
304 // race.
305 if (render_frame_host_or_null->GetMainFrame()->IsCurrent()) {
306 return committed_load_.get();
307 }
308 }
309 return nullptr;
310 }
311
ResourceLoadComplete(content::RenderFrameHost * render_frame_host,const content::GlobalRequestID & request_id,const blink::mojom::ResourceLoadInfo & resource_load_info)312 void MetricsWebContentsObserver::ResourceLoadComplete(
313 content::RenderFrameHost* render_frame_host,
314 const content::GlobalRequestID& request_id,
315 const blink::mojom::ResourceLoadInfo& resource_load_info) {
316 if (!resource_load_info.final_url.SchemeIsHTTPOrHTTPS())
317 return;
318
319 PageLoadTracker* tracker = GetTrackerOrNullForRequest(
320 request_id, render_frame_host, resource_load_info.request_destination,
321 resource_load_info.load_timing_info.request_start);
322 if (tracker) {
323 // TODO(crbug.com/721403): Fill in data reduction proxy fields when this is
324 // available in the network service.
325 // int original_content_length =
326 // was_cached ? 0
327 // : data_reduction_proxy::util::EstimateOriginalBodySize(
328 // request, lofi_decider);
329 int original_content_length = 0;
330 std::unique_ptr<data_reduction_proxy::DataReductionProxyData>
331 data_reduction_proxy_data;
332
333 const blink::mojom::CommonNetworkInfoPtr& network_info =
334 resource_load_info.network_info;
335 ExtraRequestCompleteInfo extra_request_complete_info(
336 url::Origin::Create(resource_load_info.final_url),
337 network_info->remote_endpoint.value(),
338 render_frame_host->GetFrameTreeNodeId(), resource_load_info.was_cached,
339 resource_load_info.raw_body_bytes, original_content_length,
340 std::move(data_reduction_proxy_data),
341 resource_load_info.request_destination, resource_load_info.net_error,
342 std::make_unique<net::LoadTimingInfo>(
343 resource_load_info.load_timing_info));
344 tracker->OnLoadedResource(extra_request_complete_info);
345 }
346 }
347
FrameReceivedFirstUserActivation(content::RenderFrameHost * render_frame_host)348 void MetricsWebContentsObserver::FrameReceivedFirstUserActivation(
349 content::RenderFrameHost* render_frame_host) {
350 if (committed_load_)
351 committed_load_->FrameReceivedFirstUserActivation(render_frame_host);
352 }
353
FrameDisplayStateChanged(content::RenderFrameHost * render_frame_host,bool is_display_none)354 void MetricsWebContentsObserver::FrameDisplayStateChanged(
355 content::RenderFrameHost* render_frame_host,
356 bool is_display_none) {
357 if (committed_load_)
358 committed_load_->FrameDisplayStateChanged(render_frame_host,
359 is_display_none);
360 }
361
FrameSizeChanged(content::RenderFrameHost * render_frame_host,const gfx::Size & frame_size)362 void MetricsWebContentsObserver::FrameSizeChanged(
363 content::RenderFrameHost* render_frame_host,
364 const gfx::Size& frame_size) {
365 if (committed_load_)
366 committed_load_->FrameSizeChanged(render_frame_host, frame_size);
367 }
368
OnCookiesAccessed(content::NavigationHandle * navigation,const content::CookieAccessDetails & details)369 void MetricsWebContentsObserver::OnCookiesAccessed(
370 content::NavigationHandle* navigation,
371 const content::CookieAccessDetails& details) {
372 OnCookiesAccessedImpl(details);
373 }
374
OnCookiesAccessed(content::RenderFrameHost * rfh,const content::CookieAccessDetails & details)375 void MetricsWebContentsObserver::OnCookiesAccessed(
376 content::RenderFrameHost* rfh,
377 const content::CookieAccessDetails& details) {
378 OnCookiesAccessedImpl(details);
379 }
380
OnCookiesAccessedImpl(const content::CookieAccessDetails & details)381 void MetricsWebContentsObserver::OnCookiesAccessedImpl(
382 const content::CookieAccessDetails& details) {
383 if (!committed_load_)
384 return;
385
386 // TODO(altimin): Propagate |CookieAccessDetails| further.
387 switch (details.type) {
388 case content::CookieAccessDetails::Type::kRead:
389 committed_load_->OnCookiesRead(details.url, details.first_party_url,
390 details.cookie_list,
391 details.blocked_by_policy);
392 break;
393 case content::CookieAccessDetails::Type::kChange:
394 for (const auto& cookie : details.cookie_list) {
395 committed_load_->OnCookieChange(details.url, details.first_party_url,
396 cookie, details.blocked_by_policy);
397 }
398 break;
399 }
400 }
401
DidActivatePortal(content::WebContents * predecessor_web_contents,base::TimeTicks activation_time)402 void MetricsWebContentsObserver::DidActivatePortal(
403 content::WebContents* predecessor_web_contents,
404 base::TimeTicks activation_time) {
405 // The |predecessor_web_contents| is the WebContents that instantiated the
406 // portal.
407 MetricsWebContentsObserver* predecessor_observer =
408 MetricsWebContentsObserver::FromWebContents(predecessor_web_contents);
409 // We only track the portal activation if the predecessor is also being
410 // tracked.
411 if (!committed_load_ || !predecessor_observer ||
412 !predecessor_observer->committed_load_) {
413 return;
414 }
415 committed_load_->DidActivatePortal(activation_time);
416 }
417
OnStorageAccessed(const GURL & url,const GURL & first_party_url,bool blocked_by_policy,StorageType storage_type)418 void MetricsWebContentsObserver::OnStorageAccessed(const GURL& url,
419 const GURL& first_party_url,
420 bool blocked_by_policy,
421 StorageType storage_type) {
422 if (committed_load_)
423 committed_load_->OnStorageAccessed(url, first_party_url, blocked_by_policy,
424 storage_type);
425 }
426
427 const PageLoadMetricsObserverDelegate&
GetDelegateForCommittedLoad()428 MetricsWebContentsObserver::GetDelegateForCommittedLoad() {
429 DCHECK(committed_load_);
430 return *committed_load_.get();
431 }
432
ReadyToCommitNavigation(content::NavigationHandle * navigation_handle)433 void MetricsWebContentsObserver::ReadyToCommitNavigation(
434 content::NavigationHandle* navigation_handle) {
435 if (committed_load_)
436 committed_load_->ReadyToCommitNavigation(navigation_handle);
437 }
438
DidFinishNavigation(content::NavigationHandle * navigation_handle)439 void MetricsWebContentsObserver::DidFinishNavigation(
440 content::NavigationHandle* navigation_handle) {
441 if (!navigation_handle->IsInMainFrame()) {
442 if (committed_load_ && navigation_handle->GetParentFrame() &&
443 navigation_handle->GetParentFrame()->GetMainFrame()->IsCurrent()) {
444 committed_load_->DidFinishSubFrameNavigation(navigation_handle);
445 committed_load_->metrics_update_dispatcher()->DidFinishSubFrameNavigation(
446 navigation_handle);
447 }
448 return;
449 }
450
451 // Not all navigations trigger the WillStartNavigationRequest callback (for
452 // example, navigations to about:blank). DidFinishNavigation is guaranteed to
453 // be called for every navigation, so we also update has_navigated_ here, to
454 // ensure it is set consistently for all navigations.
455 has_navigated_ = true;
456
457 std::unique_ptr<PageLoadTracker> navigation_handle_tracker(
458 std::move(provisional_loads_[navigation_handle]));
459 provisional_loads_.erase(navigation_handle);
460
461 // Ignore same-document navigations.
462 if (navigation_handle->HasCommitted() &&
463 navigation_handle->IsSameDocument()) {
464 if (navigation_handle_tracker)
465 navigation_handle_tracker->StopTracking();
466 if (committed_load_)
467 committed_load_->DidCommitSameDocumentNavigation(navigation_handle);
468 return;
469 }
470
471 // Ignore internally generated aborts for navigations with HTTP responses that
472 // don't commit, such as HTTP 204 responses and downloads.
473 if (!navigation_handle->HasCommitted() &&
474 navigation_handle->GetNetErrorCode() == net::ERR_ABORTED &&
475 navigation_handle->GetResponseHeaders()) {
476 if (navigation_handle_tracker) {
477 navigation_handle_tracker->DidInternalNavigationAbort(navigation_handle);
478 navigation_handle_tracker->StopTracking();
479 }
480 return;
481 }
482
483 if (navigation_handle->HasCommitted()) {
484 // A new navigation is committing, so finalize and destroy the tracker for
485 // the currently committed navigation.
486 FinalizeCurrentlyCommittedLoad(navigation_handle,
487 navigation_handle_tracker.get());
488 // Transfers the ownership of |committed_load_|. This |committed_load_|
489 // might be reused later when restoring the page from the cache.
490 MaybeStorePageLoadTrackerForBackForwardCache(navigation_handle,
491 std::move(committed_load_));
492 // If |navigation_handle| is a back-forward cache navigation, an associated
493 // PageLoadTracker is restored into |committed_load_|.
494 if (MaybeRestorePageLoadTrackerForBackForwardCache(navigation_handle))
495 return;
496 }
497
498 if (!navigation_handle_tracker)
499 return;
500
501 if (!ShouldTrackMainFrameNavigation(navigation_handle)) {
502 navigation_handle_tracker->StopTracking();
503 return;
504 }
505
506 if (navigation_handle->HasCommitted()) {
507 HandleCommittedNavigationForTrackedLoad(
508 navigation_handle, std::move(navigation_handle_tracker));
509 } else {
510 HandleFailedNavigationForTrackedLoad(navigation_handle,
511 std::move(navigation_handle_tracker));
512 }
513 }
514
515 // Handle a pre-commit error. Navigations that result in an error page will be
516 // ignored.
HandleFailedNavigationForTrackedLoad(content::NavigationHandle * navigation_handle,std::unique_ptr<PageLoadTracker> tracker)517 void MetricsWebContentsObserver::HandleFailedNavigationForTrackedLoad(
518 content::NavigationHandle* navigation_handle,
519 std::unique_ptr<PageLoadTracker> tracker) {
520 const base::TimeTicks now = base::TimeTicks::Now();
521 tracker->FailedProvisionalLoad(navigation_handle, now);
522
523 const net::Error error = navigation_handle->GetNetErrorCode();
524
525 // net::OK: This case occurs when the NavigationHandle finishes and reports
526 // !HasCommitted(), but reports no net::Error. This represents the navigation
527 // being stopped by the user before it was ready to commit.
528 // net::ERR_ABORTED: An aborted provisional load has error net::ERR_ABORTED.
529 const bool is_aborted_provisional_load =
530 error == net::OK || error == net::ERR_ABORTED;
531
532 // If is_aborted_provisional_load, the page end reason is not yet known, and
533 // will be updated as additional information is available from subsequent
534 // navigations.
535 tracker->NotifyPageEnd(
536 is_aborted_provisional_load ? END_OTHER : END_PROVISIONAL_LOAD_FAILED,
537 UserInitiatedInfo::NotUserInitiated(), now, true);
538
539 if (is_aborted_provisional_load)
540 aborted_provisional_loads_.push_back(std::move(tracker));
541 }
542
HandleCommittedNavigationForTrackedLoad(content::NavigationHandle * navigation_handle,std::unique_ptr<PageLoadTracker> tracker)543 void MetricsWebContentsObserver::HandleCommittedNavigationForTrackedLoad(
544 content::NavigationHandle* navigation_handle,
545 std::unique_ptr<PageLoadTracker> tracker) {
546 committed_load_ = std::move(tracker);
547 committed_load_->Commit(navigation_handle);
548 DCHECK(committed_load_->did_commit());
549
550 for (auto& observer : testing_observers_)
551 observer.OnCommit(committed_load_.get());
552
553 if (ukm_smoothness_data_.IsValid()) {
554 auto* render_frame_host = navigation_handle->GetRenderFrameHost();
555 const bool is_main_frame =
556 render_frame_host && render_frame_host->GetParent() == nullptr;
557 if (is_main_frame) {
558 committed_load_->metrics_update_dispatcher()
559 ->SetUpSharedMemoryForSmoothness(render_frame_host,
560 std::move(ukm_smoothness_data_));
561 }
562 }
563 }
564
MaybeStorePageLoadTrackerForBackForwardCache(content::NavigationHandle * next_navigation_handle,std::unique_ptr<PageLoadTracker> previously_committed_load)565 void MetricsWebContentsObserver::MaybeStorePageLoadTrackerForBackForwardCache(
566 content::NavigationHandle* next_navigation_handle,
567 std::unique_ptr<PageLoadTracker> previously_committed_load) {
568 if (!previously_committed_load)
569 return;
570
571 content::RenderFrameHost* previous_frame = content::RenderFrameHost::FromID(
572 next_navigation_handle->GetPreviousRenderFrameHostId());
573
574 // The PageLoadTracker is associated with a bfcached document if:
575 bool is_back_forward_cache =
576 // 1. the frame being navigated away from was not already deleted
577 previous_frame &&
578 // 2. the previous frame is in the BFCache
579 previous_frame->IsInBackForwardCache();
580
581 if (!is_back_forward_cache)
582 return;
583
584 previously_committed_load->OnEnterBackForwardCache();
585
586 back_forward_cached_pages_.emplace(previous_frame,
587 std::move(previously_committed_load));
588 }
589
MaybeRestorePageLoadTrackerForBackForwardCache(content::NavigationHandle * navigation_handle)590 bool MetricsWebContentsObserver::MaybeRestorePageLoadTrackerForBackForwardCache(
591 content::NavigationHandle* navigation_handle) {
592 if (!navigation_handle->IsServedFromBackForwardCache())
593 return false;
594
595 auto it =
596 back_forward_cached_pages_.find(navigation_handle->GetRenderFrameHost());
597
598 // There are some cases that the PageLoadTracker does not exist. For example,
599 // if a page is put into the cache before MetricsWebContents is created,
600 // |back_forward_cached_pages_| is empty.
601 if (it == back_forward_cached_pages_.end())
602 return false;
603
604 committed_load_ = std::move(it->second);
605 back_forward_cached_pages_.erase(it);
606 committed_load_->OnRestoreFromBackForwardCache(navigation_handle);
607
608 for (auto& observer : testing_observers_)
609 observer.OnRestoredFromBackForwardCache(committed_load_.get());
610
611 return true;
612 }
613
FinalizeCurrentlyCommittedLoad(content::NavigationHandle * newly_committed_navigation,PageLoadTracker * newly_committed_navigation_tracker)614 void MetricsWebContentsObserver::FinalizeCurrentlyCommittedLoad(
615 content::NavigationHandle* newly_committed_navigation,
616 PageLoadTracker* newly_committed_navigation_tracker) {
617 UserInitiatedInfo user_initiated_info =
618 newly_committed_navigation_tracker
619 ? newly_committed_navigation_tracker->user_initiated_info()
620 : CreateUserInitiatedInfo(newly_committed_navigation);
621
622 // Notify other loads that they may have been aborted by this committed
623 // load. is_certainly_browser_timestamp is set to false because
624 // NavigationStart() could be set in either the renderer or browser process.
625 NotifyPageEndAllLoadsWithTimestamp(
626 EndReasonForPageTransition(
627 newly_committed_navigation->GetPageTransition()),
628 user_initiated_info, newly_committed_navigation->NavigationStart(),
629 /*is_certainly_browser_timestamp=*/false);
630
631 if (committed_load_) {
632 bool is_non_user_initiated_client_redirect =
633 !IsNavigationUserInitiated(newly_committed_navigation) &&
634 (newly_committed_navigation->GetPageTransition() &
635 ui::PAGE_TRANSITION_CLIENT_REDIRECT) != 0;
636 if (is_non_user_initiated_client_redirect) {
637 committed_load_->NotifyClientRedirectTo(newly_committed_navigation);
638 }
639
640 // Ensure that any pending update gets dispatched.
641 committed_load_->metrics_update_dispatcher()->FlushPendingTimingUpdates();
642 }
643 }
644
NavigationStopped()645 void MetricsWebContentsObserver::NavigationStopped() {
646 // TODO(csharrison): Use a more user-initiated signal for STOP.
647 NotifyPageEndAllLoads(END_STOP, UserInitiatedInfo::NotUserInitiated());
648 }
649
OnInputEvent(const blink::WebInputEvent & event)650 void MetricsWebContentsObserver::OnInputEvent(
651 const blink::WebInputEvent& event) {
652 // Ignore browser navigation or reload which comes with type Undefined.
653 if (event.GetType() == blink::WebInputEvent::Type::kUndefined)
654 return;
655
656 if (committed_load_)
657 committed_load_->OnInputEvent(event);
658 }
659
FlushMetricsOnAppEnterBackground()660 void MetricsWebContentsObserver::FlushMetricsOnAppEnterBackground() {
661 // Note that, while a call to FlushMetricsOnAppEnterBackground usually
662 // indicates that the app is about to be backgrounded, there are cases where
663 // the app may not end up getting backgrounded. Thus, we should not assume
664 // anything about foreground / background state of the associated tab as part
665 // of this method call.
666
667 if (committed_load_)
668 committed_load_->FlushMetricsOnAppEnterBackground();
669 for (const auto& kv : provisional_loads_) {
670 kv.second->FlushMetricsOnAppEnterBackground();
671 }
672 for (const auto& tracker : aborted_provisional_loads_) {
673 tracker->FlushMetricsOnAppEnterBackground();
674 }
675 }
676
DidRedirectNavigation(content::NavigationHandle * navigation_handle)677 void MetricsWebContentsObserver::DidRedirectNavigation(
678 content::NavigationHandle* navigation_handle) {
679 if (!navigation_handle->IsInMainFrame())
680 return;
681 auto it = provisional_loads_.find(navigation_handle);
682 if (it == provisional_loads_.end())
683 return;
684 it->second->Redirect(navigation_handle);
685 }
686
OnVisibilityChanged(content::Visibility visibility)687 void MetricsWebContentsObserver::OnVisibilityChanged(
688 content::Visibility visibility) {
689 if (web_contents_will_soon_be_destroyed_)
690 return;
691
692 bool was_in_foreground = in_foreground_;
693 in_foreground_ = visibility == content::Visibility::VISIBLE;
694 if (in_foreground_ == was_in_foreground)
695 return;
696
697 if (in_foreground_) {
698 if (committed_load_)
699 committed_load_->PageShown();
700 for (const auto& kv : provisional_loads_) {
701 kv.second->PageShown();
702 }
703 } else {
704 if (committed_load_)
705 committed_load_->PageHidden();
706 for (const auto& kv : provisional_loads_) {
707 kv.second->PageHidden();
708 }
709 }
710
711 // As pages in back-forward cache are frozen, |back_forward_cached_pages_|
712 // don't have to be iterated here.
713 }
714
715 // This will occur when the process for the main RenderFrameHost exits, either
716 // normally or from a crash. We eagerly log data from the last committed load if
717 // we have one.
RenderProcessGone(base::TerminationStatus status)718 void MetricsWebContentsObserver::RenderProcessGone(
719 base::TerminationStatus status) {
720 // Other code paths will be run for normal renderer shutdown. Note that we
721 // sometimes get the STILL_RUNNING value on fast shutdown.
722 if (status == base::TERMINATION_STATUS_NORMAL_TERMINATION ||
723 status == base::TERMINATION_STATUS_STILL_RUNNING) {
724 return;
725 }
726
727 // RenderProcessGone is associated with the render frame host for the
728 // currently committed load. We don't know if the pending navs or aborted
729 // pending navs are associated w/ the render process that died, so we can't be
730 // sure the info should propagate to them.
731 if (committed_load_) {
732 committed_load_->NotifyPageEnd(END_RENDER_PROCESS_GONE,
733 UserInitiatedInfo::NotUserInitiated(),
734 base::TimeTicks::Now(), true);
735 }
736
737 // If this is a crash, eagerly log the aborted provisional loads and the
738 // committed load. |provisional_loads_| don't need to be destroyed here
739 // because their lifetime is tied to the NavigationHandle.
740 committed_load_.reset();
741 aborted_provisional_loads_.clear();
742 }
743
NotifyPageEndAllLoads(PageEndReason page_end_reason,UserInitiatedInfo user_initiated_info)744 void MetricsWebContentsObserver::NotifyPageEndAllLoads(
745 PageEndReason page_end_reason,
746 UserInitiatedInfo user_initiated_info) {
747 NotifyPageEndAllLoadsWithTimestamp(page_end_reason, user_initiated_info,
748 base::TimeTicks::Now(),
749 /*is_certainly_browser_timestamp=*/true);
750 }
751
NotifyPageEndAllLoadsWithTimestamp(PageEndReason page_end_reason,UserInitiatedInfo user_initiated_info,base::TimeTicks timestamp,bool is_certainly_browser_timestamp)752 void MetricsWebContentsObserver::NotifyPageEndAllLoadsWithTimestamp(
753 PageEndReason page_end_reason,
754 UserInitiatedInfo user_initiated_info,
755 base::TimeTicks timestamp,
756 bool is_certainly_browser_timestamp) {
757 if (committed_load_) {
758 committed_load_->NotifyPageEnd(page_end_reason, user_initiated_info,
759 timestamp, is_certainly_browser_timestamp);
760 }
761 for (const auto& kv : provisional_loads_) {
762 kv.second->NotifyPageEnd(page_end_reason, user_initiated_info, timestamp,
763 is_certainly_browser_timestamp);
764 }
765 for (const auto& tracker : aborted_provisional_loads_) {
766 if (tracker->IsLikelyProvisionalAbort(timestamp)) {
767 tracker->UpdatePageEnd(page_end_reason, user_initiated_info, timestamp,
768 is_certainly_browser_timestamp);
769 }
770 }
771 aborted_provisional_loads_.clear();
772 }
773
774 std::unique_ptr<PageLoadTracker>
NotifyAbortedProvisionalLoadsNewNavigation(content::NavigationHandle * new_navigation,UserInitiatedInfo user_initiated_info)775 MetricsWebContentsObserver::NotifyAbortedProvisionalLoadsNewNavigation(
776 content::NavigationHandle* new_navigation,
777 UserInitiatedInfo user_initiated_info) {
778 // If there are multiple aborted loads that can be attributed to this one,
779 // just count the latest one for simplicity. Other loads will fall into the
780 // OTHER bucket, though there shouldn't be very many.
781 if (aborted_provisional_loads_.empty())
782 return nullptr;
783 if (aborted_provisional_loads_.size() > 1)
784 RecordInternalError(ERR_NAVIGATION_SIGNALS_MULIPLE_ABORTED_LOADS);
785
786 std::unique_ptr<PageLoadTracker> last_aborted_load =
787 std::move(aborted_provisional_loads_.back());
788 aborted_provisional_loads_.pop_back();
789
790 base::TimeTicks timestamp = new_navigation->NavigationStart();
791 if (last_aborted_load->IsLikelyProvisionalAbort(timestamp)) {
792 last_aborted_load->UpdatePageEnd(
793 EndReasonForPageTransition(new_navigation->GetPageTransition()),
794 user_initiated_info, timestamp, false);
795 }
796
797 aborted_provisional_loads_.clear();
798 return last_aborted_load;
799 }
800
OnTimingUpdated(content::RenderFrameHost * render_frame_host,mojom::PageLoadTimingPtr timing,mojom::FrameMetadataPtr metadata,mojom::PageLoadFeaturesPtr new_features,const std::vector<mojom::ResourceDataUpdatePtr> & resources,mojom::FrameRenderDataUpdatePtr render_data,mojom::CpuTimingPtr cpu_timing,mojom::DeferredResourceCountsPtr new_deferred_resource_data,mojom::InputTimingPtr input_timing_delta,const blink::MobileFriendliness & mobile_friendliness)801 void MetricsWebContentsObserver::OnTimingUpdated(
802 content::RenderFrameHost* render_frame_host,
803 mojom::PageLoadTimingPtr timing,
804 mojom::FrameMetadataPtr metadata,
805 mojom::PageLoadFeaturesPtr new_features,
806 const std::vector<mojom::ResourceDataUpdatePtr>& resources,
807 mojom::FrameRenderDataUpdatePtr render_data,
808 mojom::CpuTimingPtr cpu_timing,
809 mojom::DeferredResourceCountsPtr new_deferred_resource_data,
810 mojom::InputTimingPtr input_timing_delta,
811 const blink::MobileFriendliness& mobile_friendliness) {
812 // We may receive notifications from frames that have been navigated away
813 // from. We simply ignore them.
814 // TODO(crbug.com/1061060): We should not ignore page timings if the page is
815 // in bfcache.
816 if (!render_frame_host->GetMainFrame()->IsCurrent()) {
817 RecordInternalError(ERR_IPC_FROM_WRONG_FRAME);
818 return;
819 }
820
821 const bool is_main_frame = (render_frame_host->GetParent() == nullptr);
822 if (is_main_frame) {
823 if (DoesTimingUpdateHaveError())
824 return;
825 } else if (!committed_load_) {
826 RecordInternalError(ERR_SUBFRAME_IPC_WITH_NO_RELEVANT_LOAD);
827 }
828
829 if (committed_load_) {
830 committed_load_->metrics_update_dispatcher()->UpdateMetrics(
831 render_frame_host, std::move(timing), std::move(metadata),
832 std::move(new_features), resources, std::move(render_data),
833 std::move(cpu_timing), std::move(new_deferred_resource_data),
834 std::move(input_timing_delta), std::move(mobile_friendliness));
835 }
836 }
837
DoesTimingUpdateHaveError()838 bool MetricsWebContentsObserver::DoesTimingUpdateHaveError() {
839 // While timings arriving for the wrong frame are expected, we do not expect
840 // any of the errors below for main frames. Thus, we track occurrences of
841 // all errors below, rather than returning early after encountering an
842 // error.
843 // TODO(crbug/1061090): Update page load metrics IPC validation to ues
844 // mojo::ReportBadMessage.
845 bool error = false;
846 if (!committed_load_) {
847 RecordInternalError(ERR_IPC_WITH_NO_RELEVANT_LOAD);
848 error = true;
849 }
850
851 if (!web_contents()->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()) {
852 RecordInternalError(ERR_IPC_FROM_BAD_URL_SCHEME);
853 error = true;
854 }
855 return error;
856 }
857
UpdateTiming(mojom::PageLoadTimingPtr timing,mojom::FrameMetadataPtr metadata,mojom::PageLoadFeaturesPtr new_features,std::vector<mojom::ResourceDataUpdatePtr> resources,mojom::FrameRenderDataUpdatePtr render_data,mojom::CpuTimingPtr cpu_timing,mojom::DeferredResourceCountsPtr new_deferred_resource_data,mojom::InputTimingPtr input_timing_delta,const blink::MobileFriendliness & mobile_friendliness)858 void MetricsWebContentsObserver::UpdateTiming(
859 mojom::PageLoadTimingPtr timing,
860 mojom::FrameMetadataPtr metadata,
861 mojom::PageLoadFeaturesPtr new_features,
862 std::vector<mojom::ResourceDataUpdatePtr> resources,
863 mojom::FrameRenderDataUpdatePtr render_data,
864 mojom::CpuTimingPtr cpu_timing,
865 mojom::DeferredResourceCountsPtr new_deferred_resource_data,
866 mojom::InputTimingPtr input_timing_delta,
867 const blink::MobileFriendliness& mobile_friendliness) {
868 content::RenderFrameHost* render_frame_host =
869 page_load_metrics_receiver_.GetCurrentTargetFrame();
870 OnTimingUpdated(render_frame_host, std::move(timing), std::move(metadata),
871 std::move(new_features), resources, std::move(render_data),
872 std::move(cpu_timing), std::move(new_deferred_resource_data),
873 std::move(input_timing_delta),
874 std::move(mobile_friendliness));
875 }
876
SetUpSharedMemoryForSmoothness(base::ReadOnlySharedMemoryRegion shared_memory)877 void MetricsWebContentsObserver::SetUpSharedMemoryForSmoothness(
878 base::ReadOnlySharedMemoryRegion shared_memory) {
879 content::RenderFrameHost* render_frame_host =
880 page_load_metrics_receiver_.GetCurrentTargetFrame();
881 const bool is_main_frame = render_frame_host->GetParent() == nullptr;
882 if (!is_main_frame) {
883 // TODO(1115136): Merge smoothness metrics from OOPIFs with the main-frame.
884 return;
885 }
886
887 if (committed_load_) {
888 committed_load_->metrics_update_dispatcher()
889 ->SetUpSharedMemoryForSmoothness(render_frame_host,
890 std::move(shared_memory));
891 } else {
892 ukm_smoothness_data_ = std::move(shared_memory);
893 }
894 }
895
ShouldTrackMainFrameNavigation(content::NavigationHandle * navigation_handle) const896 bool MetricsWebContentsObserver::ShouldTrackMainFrameNavigation(
897 content::NavigationHandle* navigation_handle) const {
898 DCHECK(navigation_handle->IsInMainFrame());
899 DCHECK(!navigation_handle->HasCommitted() ||
900 !navigation_handle->IsSameDocument());
901 // Ignore non-HTTP schemes (e.g. chrome://).
902 if (!navigation_handle->GetURL().SchemeIsHTTPOrHTTPS())
903 return false;
904
905 // Ignore NTP loads.
906 if (embedder_interface_->IsNewTabPageUrl(navigation_handle->GetURL()))
907 return false;
908
909 // The navigation served from the back-forward cache will use the previously
910 // created tracker for the document.
911 if (navigation_handle->IsServedFromBackForwardCache())
912 return false;
913
914 if (navigation_handle->HasCommitted()) {
915 // Ignore Chrome error pages (e.g. No Internet connection).
916 if (navigation_handle->IsErrorPage())
917 return false;
918
919 // Ignore network error pages (e.g. 4xx, 5xx).
920 int http_status_code = GetHttpStatusCode(navigation_handle);
921 if (http_status_code > 0 &&
922 (http_status_code < 200 || http_status_code >= 400))
923 return false;
924 }
925
926 return true;
927 }
928
OnBrowserFeatureUsage(content::RenderFrameHost * render_frame_host,const mojom::PageLoadFeatures & new_features)929 void MetricsWebContentsObserver::OnBrowserFeatureUsage(
930 content::RenderFrameHost* render_frame_host,
931 const mojom::PageLoadFeatures& new_features) {
932 // Since this call is coming directly from the browser, it should not pass us
933 // data from frames that have already been navigated away from.
934 DCHECK(render_frame_host->GetMainFrame()->IsCurrent());
935
936 if (!committed_load_) {
937 RecordInternalError(ERR_BROWSER_USAGE_WITH_NO_RELEVANT_LOAD);
938 return;
939 }
940
941 committed_load_->metrics_update_dispatcher()->UpdateFeatures(
942 render_frame_host, new_features);
943 }
944
AddTestingObserver(TestingObserver * observer)945 void MetricsWebContentsObserver::AddTestingObserver(TestingObserver* observer) {
946 if (!testing_observers_.HasObserver(observer))
947 testing_observers_.AddObserver(observer);
948 }
949
RemoveTestingObserver(TestingObserver * observer)950 void MetricsWebContentsObserver::RemoveTestingObserver(
951 TestingObserver* observer) {
952 testing_observers_.RemoveObserver(observer);
953 }
954
TestingObserver(content::WebContents * web_contents)955 MetricsWebContentsObserver::TestingObserver::TestingObserver(
956 content::WebContents* web_contents)
957 : observer_(page_load_metrics::MetricsWebContentsObserver::FromWebContents(
958 web_contents)) {
959 observer_->AddTestingObserver(this);
960 }
961
~TestingObserver()962 MetricsWebContentsObserver::TestingObserver::~TestingObserver() {
963 if (observer_) {
964 observer_->RemoveTestingObserver(this);
965 observer_ = nullptr;
966 }
967 }
968
OnGoingAway()969 void MetricsWebContentsObserver::TestingObserver::OnGoingAway() {
970 observer_ = nullptr;
971 }
972
973 const PageLoadMetricsObserverDelegate&
GetDelegateForCommittedLoad()974 MetricsWebContentsObserver::TestingObserver::GetDelegateForCommittedLoad() {
975 return observer_->GetDelegateForCommittedLoad();
976 }
977
BroadcastEventToObservers(const void * const event_key)978 void MetricsWebContentsObserver::BroadcastEventToObservers(
979 const void* const event_key) {
980 if (committed_load_)
981 committed_load_->BroadcastEventToObservers(event_key);
982 }
983
984 WEB_CONTENTS_USER_DATA_KEY_IMPL(MetricsWebContentsObserver)
985
986 } // namespace page_load_metrics
987