blob: 50baa4a834d5c5e9131c134be2752a9ac78bbd5d [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/page_load_metrics/browser/page_load_metrics_util.h"
#include <algorithm>
#include <string_view>
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/page_load_metrics/browser/features.h"
#include "components/page_load_metrics/common/page_load_timing.h"
#include "components/page_load_metrics/common/page_visit_final_status.h"
#include "net/base/url_util.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
namespace page_load_metrics {
namespace {
bool IsBackgroundAbort(const PageLoadMetricsObserverDelegate& delegate) {
if (!delegate.StartedInForeground() || !delegate.GetTimeToFirstBackground())
return false;
if (!delegate.GetTimeToPageEnd())
return true;
return delegate.GetTimeToFirstBackground() <= delegate.GetTimeToPageEnd();
}
PageAbortReason GetAbortReasonForEndReason(PageEndReason end_reason) {
switch (end_reason) {
case END_RELOAD:
return ABORT_RELOAD;
case END_FORWARD_BACK:
return ABORT_FORWARD_BACK;
case END_NEW_NAVIGATION:
return ABORT_NEW_NAVIGATION;
case END_STOP:
return ABORT_STOP;
case END_CLOSE:
return ABORT_CLOSE;
case END_OTHER:
return ABORT_OTHER;
default:
return ABORT_NONE;
}
}
// Common helper for QueryContainsComponent and QueryContainsComponentPrefix.
bool QueryContainsComponentHelper(std::string_view query,
std::string_view component,
bool component_is_prefix) {
if (query.empty() || component.empty() ||
component.length() > query.length()) {
return false;
}
// Ensures that the first character of |query| is not a query or fragment
// delimiter character (? or #). Including it can break the later test for
// |component| being at the start of the query string.
// Note: This heuristic can cause a component string that starts with one of
// these characters to not match a query string which contains it at the
// beginning.
const std::string_view trimmed_query =
base::TrimString(query, "?#", base::TrimPositions::TRIM_LEADING);
// We shouldn't try to find matches beyond the point where there aren't enough
// characters left in query to fully match the component.
const size_t last_search_start = trimmed_query.length() - component.length();
// We need to search for matches in a loop, rather than stopping at the first
// match, because we may initially match a substring that isn't a full query
// string component. Consider, for instance, the query string 'ab=cd&b=c'. If
// we search for component 'b=c', the first substring match will be characters
// 1-3 (zero-based) in the query string. However, this isn't a full component
// (the full component is ab=cd) so the match will fail. Thus, we must
// continue our search to find the second substring match, which in the
// example is at characters 6-8 (the end of the query string) and is a
// successful component match.
for (size_t start_offset = 0; start_offset <= last_search_start;
start_offset += component.length()) {
start_offset = trimmed_query.find(component, start_offset);
if (start_offset == std::string::npos) {
// We searched to end of string and did not find a match.
return false;
}
// Verify that the character prior to the component is valid (either we're
// at the beginning of the query string, or are preceded by an ampersand).
if (start_offset != 0 && trimmed_query[start_offset - 1] != '&') {
continue;
}
if (!component_is_prefix) {
// Verify that the character after the component substring is valid
// (either we're at the end of the query string, or are followed by an
// ampersand).
const size_t after_offset = start_offset + component.length();
if (after_offset < trimmed_query.length() &&
trimmed_query[after_offset] != '&') {
continue;
}
}
return true;
}
return false;
}
// Returns the category ID given a string if it matches the configured pattern.
std::optional<uint32_t> GetCategoryId(const std::string& category) {
auto category_prefix = features::kBeaconLeakageLoggingCategoryPrefix.Get();
if (category_prefix.empty() || !category.starts_with(category_prefix)) {
return std::nullopt;
}
uint32_t category_id;
if (!base::StringToUint(category.substr(category_prefix.size()),
&category_id)) {
return std::nullopt;
}
return category_id;
}
} // namespace
void UmaMaxCumulativeShiftScoreHistogram10000x(
const std::string& name,
const page_load_metrics::NormalizedCLSData& normalized_cls_data) {
base::UmaHistogramCustomCounts(
name,
page_load_metrics::LayoutShiftUmaValue10000(
normalized_cls_data.session_windows_gap1000ms_max5000ms_max_cls),
1, 24000, 50);
}
bool WasStartedInForegroundOptionalEventInForeground(
const std::optional<base::TimeDelta>& event,
const PageLoadMetricsObserverDelegate& delegate) {
return delegate.StartedInForeground() && event &&
(!delegate.GetTimeToFirstBackground() ||
event.value() <= delegate.GetTimeToFirstBackground().value());
}
// There is a copy of this function in prerender_page_load_metrics_observer.cc.
// Please keep this consistent with the function.
bool WasActivatedInForegroundOptionalEventInForeground(
const std::optional<base::TimeDelta>& event,
const PageLoadMetricsObserverDelegate& delegate) {
return delegate.WasPrerenderedThenActivatedInForeground() && event &&
(!delegate.GetTimeToFirstBackground() ||
event.value() <= delegate.GetTimeToFirstBackground().value());
}
bool WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(
const std::optional<base::TimeDelta>& event,
const PageLoadMetricsObserverDelegate& delegate,
size_t index) {
const auto& back_forward_cache_restore =
delegate.GetBackForwardCacheRestore(index);
std::optional<base::TimeDelta> first_background_time =
back_forward_cache_restore.first_background_time;
return back_forward_cache_restore.was_in_foreground && event &&
(!first_background_time ||
event.value() <= first_background_time.value());
}
bool WasStartedInBackgroundOptionalEventInForeground(
const std::optional<base::TimeDelta>& event,
const PageLoadMetricsObserverDelegate& delegate) {
return !delegate.StartedInForeground() && event &&
delegate.GetTimeToFirstForeground() &&
delegate.GetTimeToFirstForeground().value() <= event.value() &&
(!delegate.GetTimeToFirstBackground() ||
event.value() <= delegate.GetTimeToFirstBackground().value());
}
bool WasInForeground(const PageLoadMetricsObserverDelegate& delegate) {
return delegate.StartedInForeground() || delegate.GetTimeToFirstForeground();
}
std::optional<base::TimeDelta> GetNonPrerenderingBackgroundStartTiming(
const PageLoadMetricsObserverDelegate& delegate) {
switch (delegate.GetPrerenderingState()) {
case PrerenderingState::kNoPrerendering:
case PrerenderingState::kInPreview:
if (delegate.StartedInForeground()) {
return delegate.GetTimeToFirstBackground();
} else {
return base::Seconds(0);
}
case PrerenderingState::kInPrerendering:
case PrerenderingState::kActivatedNoActivationStart:
return std::nullopt;
case PrerenderingState::kActivated:
if (delegate.GetVisibilityAtActivation() == PageVisibility::kForeground) {
return delegate.GetTimeToFirstBackground();
} else {
return delegate.GetActivationStart();
}
}
}
bool EventOccurredBeforeNonPrerenderingBackgroundStart(
const PageLoadMetricsObserverDelegate& delegate,
const base::TimeDelta& event) {
// If background start is nullopt, it'll must be greater than already
// occurred event.
const base::TimeDelta bg_start =
GetNonPrerenderingBackgroundStartTiming(delegate).value_or(
base::TimeDelta::Max());
return event < bg_start;
}
// Currently, multiple implementations of PageLoadMetricsObserver is ongoing.
// We'll left the old version for a while.
// TODO(crbug.com/40222513): Use the above version and delete this.
bool EventOccurredBeforeNonPrerenderingBackgroundStart(
const PageLoadMetricsObserverDelegate& delegate,
const page_load_metrics::mojom::PageLoadTiming& timing,
const base::TimeDelta& event) {
return EventOccurredBeforeNonPrerenderingBackgroundStart(delegate, event);
}
base::TimeDelta CorrectEventAsNavigationOrActivationOrigined(
const PageLoadMetricsObserverDelegate& delegate,
const base::TimeDelta& event) {
base::TimeDelta zero = base::Seconds(0);
switch (delegate.GetPrerenderingState()) {
case PrerenderingState::kNoPrerendering:
case PrerenderingState::kInPreview:
return event;
case PrerenderingState::kInPrerendering:
case PrerenderingState::kActivatedNoActivationStart:
return zero;
case PrerenderingState::kActivated: {
base::TimeDelta corrected = event - delegate.GetActivationStart().value();
// If the event occurred before activation, return 0.
return std::max(corrected, zero);
}
}
}
// Currently, multiple implementations of PageLoadMetricsObserver is ongoing.
// We'll left the old version for a while.
// TODO(crbug.com/40222513): Use the above version and delete this.
base::TimeDelta CorrectEventAsNavigationOrActivationOrigined(
const PageLoadMetricsObserverDelegate& delegate,
const page_load_metrics::mojom::PageLoadTiming& timing,
const base::TimeDelta& event) {
return CorrectEventAsNavigationOrActivationOrigined(delegate, event);
}
PageAbortInfo GetPageAbortInfo(
const PageLoadMetricsObserverDelegate& delegate) {
if (IsBackgroundAbort(delegate)) {
// Though most cases where a tab is backgrounded are user initiated, we
// can't be certain that we were backgrounded due to a user action. For
// example, on Android, the screen times out after a period of inactivity,
// resulting in a non-user-initiated backgrounding.
return {ABORT_BACKGROUND, UserInitiatedInfo::NotUserInitiated(),
delegate.GetTimeToFirstBackground().value()};
}
PageAbortReason abort_reason =
GetAbortReasonForEndReason(delegate.GetPageEndReason());
if (abort_reason == ABORT_NONE)
return PageAbortInfo();
return {abort_reason, delegate.GetPageEndUserInitiatedInfo(),
delegate.GetTimeToPageEnd().value()};
}
std::optional<base::TimeDelta> GetInitialForegroundDuration(
const PageLoadMetricsObserverDelegate& delegate,
base::TimeTicks app_background_time) {
if (!delegate.StartedInForeground())
return std::nullopt;
std::optional<base::TimeDelta> time_on_page = OptionalMin(
delegate.GetTimeToFirstBackground(), delegate.GetTimeToPageEnd());
// If we don't have a time_on_page value yet, and we have an app background
// time, use the app background time as our end time. This addresses cases
// where the Chrome app is backgrounded before the page load is complete, on
// platforms where Chrome may be killed once it goes into the background
// (Android). In these cases, we use the app background time as the 'end
// time'.
if (!time_on_page && !app_background_time.is_null()) {
time_on_page = app_background_time - delegate.GetNavigationStart();
}
return time_on_page;
}
bool DidObserveLoadingBehaviorInAnyFrame(
const PageLoadMetricsObserverDelegate& delegate,
blink::LoadingBehaviorFlag behavior) {
const int all_frame_loading_behavior_flags =
delegate.GetMainFrameMetadata().behavior_flags |
delegate.GetSubframeMetadata().behavior_flags;
return (all_frame_loading_behavior_flags & behavior) != 0;
}
bool IsZstdUrl(const GURL& url) {
return url.DomainIs("facebook.com") || url.DomainIs("instagram.com") ||
url.DomainIs("whatsapp.com") || url.DomainIs("messenger.com");
}
bool QueryContainsComponent(std::string_view query,
std::string_view component) {
return QueryContainsComponentHelper(query, component, false);
}
bool QueryContainsComponentPrefix(std::string_view query,
std::string_view component) {
return QueryContainsComponentHelper(query, component, true);
}
int64_t LayoutShiftUkmValue(float shift_score) {
// Report (shift_score * 100) as an int in the range [0, 1000].
return static_cast<int>(roundf(std::min(shift_score, 10.0f) * 100.0f));
}
int32_t LayoutShiftUmaValue(float shift_score) {
// Report (shift_score * 10) as an int in the range [0, 100].
return static_cast<int>(roundf(std::min(shift_score, 10.0f) * 10.0f));
}
int32_t LayoutShiftUmaValue10000(float shift_score) {
// Report (shift_score * 10000) as an int in the range [0, 100000].
return static_cast<int>(roundf(std::min(shift_score, 10.0f) * 10000.0f));
}
PageVisitFinalStatus RecordPageVisitFinalStatusForTiming(
const page_load_metrics::mojom::PageLoadTiming& timing,
const PageLoadMetricsObserverDelegate& delegate,
ukm::SourceId source_id) {
PageVisitFinalStatus page_visit_status =
PageVisitFinalStatus::kNeverForegrounded;
if (page_load_metrics::WasInForeground(delegate)) {
page_visit_status = timing.paint_timing->first_contentful_paint.has_value()
? PageVisitFinalStatus::kReachedFCP
: PageVisitFinalStatus::kAborted;
}
ukm::builders::UserPerceivedPageVisit pageVisitBuilder(source_id);
pageVisitBuilder.SetPageVisitFinalStatus(static_cast<int>(page_visit_status));
pageVisitBuilder.Record(ukm::UkmRecorder::Get());
return page_visit_status;
}
std::optional<uint32_t> GetCategoryIdFromUrl(const GURL& url) {
std::string category;
if (net::GetValueForKeyInQuery(
url, features::kBeaconLeakageLoggingCategoryParamName.Get(),
&category)) {
return GetCategoryId(category);
}
return std::nullopt;
}
bool IsServiceWorkerControlled(
const PageLoadMetricsObserverDelegate& delegate) {
return (delegate.GetMainFrameMetadata().behavior_flags &
blink::LoadingBehaviorFlag::
kLoadingBehaviorServiceWorkerControlled) != 0;
}
} // namespace page_load_metrics