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/page_load_metrics_util.h"
6 
7 #include <algorithm>
8 
9 #include "components/page_load_metrics/common/page_load_timing.h"
10 
11 namespace page_load_metrics {
12 
13 namespace {
14 
IsBackgroundAbort(const PageLoadMetricsObserverDelegate & delegate)15 bool IsBackgroundAbort(const PageLoadMetricsObserverDelegate& delegate) {
16   if (!delegate.StartedInForeground() || !delegate.GetFirstBackgroundTime())
17     return false;
18 
19   if (!delegate.GetPageEndTime())
20     return true;
21 
22   return delegate.GetFirstBackgroundTime() <= delegate.GetPageEndTime();
23 }
24 
GetAbortReasonForEndReason(PageEndReason end_reason)25 PageAbortReason GetAbortReasonForEndReason(PageEndReason end_reason) {
26   switch (end_reason) {
27     case END_RELOAD:
28       return ABORT_RELOAD;
29     case END_FORWARD_BACK:
30       return ABORT_FORWARD_BACK;
31     case END_NEW_NAVIGATION:
32       return ABORT_NEW_NAVIGATION;
33     case END_STOP:
34       return ABORT_STOP;
35     case END_CLOSE:
36       return ABORT_CLOSE;
37     case END_OTHER:
38       return ABORT_OTHER;
39     default:
40       return ABORT_NONE;
41   }
42 }
43 
44 // Common helper for QueryContainsComponent and QueryContainsComponentPrefix.
QueryContainsComponentHelper(const base::StringPiece query,const base::StringPiece component,bool component_is_prefix)45 bool QueryContainsComponentHelper(const base::StringPiece query,
46                                   const base::StringPiece component,
47                                   bool component_is_prefix) {
48   if (query.empty() || component.empty() ||
49       component.length() > query.length()) {
50     return false;
51   }
52 
53   // Ensures that the first character of |query| is not a query or fragment
54   // delimiter character (? or #). Including it can break the later test for
55   // |component| being at the start of the query string.
56   // Note: This heuristic can cause a component string that starts with one of
57   // these characters to not match a query string which contains it at the
58   // beginning.
59   const base::StringPiece trimmed_query =
60       base::TrimString(query, "?#", base::TrimPositions::TRIM_LEADING);
61 
62   // We shouldn't try to find matches beyond the point where there aren't enough
63   // characters left in query to fully match the component.
64   const size_t last_search_start = trimmed_query.length() - component.length();
65 
66   // We need to search for matches in a loop, rather than stopping at the first
67   // match, because we may initially match a substring that isn't a full query
68   // string component. Consider, for instance, the query string 'ab=cd&b=c'. If
69   // we search for component 'b=c', the first substring match will be characters
70   // 1-3 (zero-based) in the query string. However, this isn't a full component
71   // (the full component is ab=cd) so the match will fail. Thus, we must
72   // continue our search to find the second substring match, which in the
73   // example is at characters 6-8 (the end of the query string) and is a
74   // successful component match.
75   for (size_t start_offset = 0; start_offset <= last_search_start;
76        start_offset += component.length()) {
77     start_offset = trimmed_query.find(component, start_offset);
78     if (start_offset == std::string::npos) {
79       // We searched to end of string and did not find a match.
80       return false;
81     }
82     // Verify that the character prior to the component is valid (either we're
83     // at the beginning of the query string, or are preceded by an ampersand).
84     if (start_offset != 0 && trimmed_query[start_offset - 1] != '&') {
85       continue;
86     }
87     if (!component_is_prefix) {
88       // Verify that the character after the component substring is valid
89       // (either we're at the end of the query string, or are followed by an
90       // ampersand).
91       const size_t after_offset = start_offset + component.length();
92       if (after_offset < trimmed_query.length() &&
93           trimmed_query[after_offset] != '&') {
94         continue;
95       }
96     }
97     return true;
98   }
99   return false;
100 }
101 
102 }  // namespace
103 
WasStartedInForegroundOptionalEventInForeground(const base::Optional<base::TimeDelta> & event,const PageLoadMetricsObserverDelegate & delegate)104 bool WasStartedInForegroundOptionalEventInForeground(
105     const base::Optional<base::TimeDelta>& event,
106     const PageLoadMetricsObserverDelegate& delegate) {
107   return delegate.StartedInForeground() && event &&
108          (!delegate.GetFirstBackgroundTime() ||
109           event.value() <= delegate.GetFirstBackgroundTime().value());
110 }
111 
WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(const base::Optional<base::TimeDelta> & event,const PageLoadMetricsObserverDelegate & delegate,size_t index)112 bool WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(
113     const base::Optional<base::TimeDelta>& event,
114     const PageLoadMetricsObserverDelegate& delegate,
115     size_t index) {
116   const auto& back_forward_cache_restore =
117       delegate.GetBackForwardCacheRestore(index);
118   base::Optional<base::TimeDelta> first_background_time =
119       back_forward_cache_restore.first_background_time;
120   return back_forward_cache_restore.was_in_foreground && event &&
121          (!first_background_time ||
122           event.value() <= first_background_time.value());
123 }
124 
WasStartedInBackgroundOptionalEventInForeground(const base::Optional<base::TimeDelta> & event,const PageLoadMetricsObserverDelegate & delegate)125 bool WasStartedInBackgroundOptionalEventInForeground(
126     const base::Optional<base::TimeDelta>& event,
127     const PageLoadMetricsObserverDelegate& delegate) {
128   return !delegate.StartedInForeground() && event &&
129          delegate.GetFirstForegroundTime() &&
130          delegate.GetFirstForegroundTime().value() <= event.value() &&
131          (!delegate.GetFirstBackgroundTime() ||
132           event.value() <= delegate.GetFirstBackgroundTime().value());
133 }
134 
WasInForeground(const PageLoadMetricsObserverDelegate & delegate)135 bool WasInForeground(const PageLoadMetricsObserverDelegate& delegate) {
136   return delegate.StartedInForeground() || delegate.GetFirstForegroundTime();
137 }
138 
GetPageAbortInfo(const PageLoadMetricsObserverDelegate & delegate)139 PageAbortInfo GetPageAbortInfo(
140     const PageLoadMetricsObserverDelegate& delegate) {
141   if (IsBackgroundAbort(delegate)) {
142     // Though most cases where a tab is backgrounded are user initiated, we
143     // can't be certain that we were backgrounded due to a user action. For
144     // example, on Android, the screen times out after a period of inactivity,
145     // resulting in a non-user-initiated backgrounding.
146     return {ABORT_BACKGROUND, UserInitiatedInfo::NotUserInitiated(),
147             delegate.GetFirstBackgroundTime().value()};
148   }
149 
150   PageAbortReason abort_reason =
151       GetAbortReasonForEndReason(delegate.GetPageEndReason());
152   if (abort_reason == ABORT_NONE)
153     return PageAbortInfo();
154 
155   return {abort_reason, delegate.GetPageEndUserInitiatedInfo(),
156           delegate.GetPageEndTime().value()};
157 }
158 
GetInitialForegroundDuration(const PageLoadMetricsObserverDelegate & delegate,base::TimeTicks app_background_time)159 base::Optional<base::TimeDelta> GetInitialForegroundDuration(
160     const PageLoadMetricsObserverDelegate& delegate,
161     base::TimeTicks app_background_time) {
162   if (!delegate.StartedInForeground())
163     return base::Optional<base::TimeDelta>();
164 
165   base::Optional<base::TimeDelta> time_on_page =
166       OptionalMin(delegate.GetFirstBackgroundTime(), delegate.GetPageEndTime());
167 
168   // If we don't have a time_on_page value yet, and we have an app background
169   // time, use the app background time as our end time. This addresses cases
170   // where the Chrome app is backgrounded before the page load is complete, on
171   // platforms where Chrome may be killed once it goes into the background
172   // (Android). In these cases, we use the app background time as the 'end
173   // time'.
174   if (!time_on_page && !app_background_time.is_null()) {
175     time_on_page = app_background_time - delegate.GetNavigationStart();
176   }
177   return time_on_page;
178 }
179 
DidObserveLoadingBehaviorInAnyFrame(const PageLoadMetricsObserverDelegate & delegate,blink::LoadingBehaviorFlag behavior)180 bool DidObserveLoadingBehaviorInAnyFrame(
181     const PageLoadMetricsObserverDelegate& delegate,
182     blink::LoadingBehaviorFlag behavior) {
183   const int all_frame_loading_behavior_flags =
184       delegate.GetMainFrameMetadata().behavior_flags |
185       delegate.GetSubframeMetadata().behavior_flags;
186 
187   return (all_frame_loading_behavior_flags & behavior) != 0;
188 }
189 
IsGoogleSearchHostname(const GURL & url)190 bool IsGoogleSearchHostname(const GURL& url) {
191   base::Optional<std::string> result =
192       page_load_metrics::GetGoogleHostnamePrefix(url);
193   return result && result.value() == "www";
194 }
195 
IsGoogleSearchResultUrl(const GURL & url)196 bool IsGoogleSearchResultUrl(const GURL& url) {
197   // NOTE: we do not require 'q=' in the query, as AJAXy search may instead
198   // store the query in the URL fragment.
199   if (!IsGoogleSearchHostname(url)) {
200     return false;
201   }
202 
203   if (!QueryContainsComponentPrefix(url.query_piece(), "q=") &&
204       !QueryContainsComponentPrefix(url.ref_piece(), "q=")) {
205     return false;
206   }
207 
208   const base::StringPiece path = url.path_piece();
209   return path == "/search" || path == "/webhp" || path == "/custom" ||
210          path == "/";
211 }
212 
IsGoogleSearchRedirectorUrl(const GURL & url)213 bool IsGoogleSearchRedirectorUrl(const GURL& url) {
214   if (!IsGoogleSearchHostname(url))
215     return false;
216 
217   // The primary search redirector.  Google search result redirects are
218   // differentiated from other general google redirects by 'source=web' in the
219   // query string.
220   if (url.path_piece() == "/url" && url.has_query() &&
221       QueryContainsComponent(url.query_piece(), "source=web")) {
222     return true;
223   }
224 
225   // Intent-based navigations from search are redirected through a second
226   // redirector, which receives its redirect URL in the fragment/hash/ref
227   // portion of the URL (the portion after '#'). We don't check for the presence
228   // of certain params in the ref since this redirector is only used for
229   // redirects from search.
230   return url.path_piece() == "/searchurl/r.html" && url.has_ref();
231 }
232 
QueryContainsComponent(const base::StringPiece query,const base::StringPiece component)233 bool QueryContainsComponent(const base::StringPiece query,
234                             const base::StringPiece component) {
235   return QueryContainsComponentHelper(query, component, false);
236 }
237 
QueryContainsComponentPrefix(const base::StringPiece query,const base::StringPiece component)238 bool QueryContainsComponentPrefix(const base::StringPiece query,
239                                   const base::StringPiece component) {
240   return QueryContainsComponentHelper(query, component, true);
241 }
242 
LayoutShiftUkmValue(float shift_score)243 int64_t LayoutShiftUkmValue(float shift_score) {
244   // Report (shift_score * 100) as an int in the range [0, 1000].
245   return static_cast<int>(roundf(std::min(shift_score, 10.0f) * 100.0f));
246 }
247 
LayoutShiftUmaValue(float shift_score)248 int32_t LayoutShiftUmaValue(float shift_score) {
249   // Report (shift_score * 10) as an int in the range [0, 100].
250   return static_cast<int>(roundf(std::min(shift_score, 10.0f) * 10.0f));
251 }
252 
253 }  // namespace page_load_metrics
254