blob: 3ed14d4251786de9131284a0b00ae2e2e3c654f0 [file] [log] [blame]
// Copyright 2021 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/preloading/prerender/prerender_metrics.h"
#include "base/containers/flat_map.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/string_util.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/public/browser/prerender_trigger_type.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
namespace content {
namespace {
// Do not add new value.
// These values are used to persists sparse metrics to logs.
enum HeaderMismatchType : uint32_t {
kMatch = 0,
kMissingInPrerendering = 1,
kMissingInActivation = 2,
kValueMismatch = 3,
kMaxValue = kValueMismatch
};
PrerenderCancelledInterface GetCancelledInterfaceType(
const std::string& interface_name) {
if (interface_name == "device.mojom.GamepadHapticsManager")
return PrerenderCancelledInterface::kGamepadHapticsManager;
else if (interface_name == "device.mojom.GamepadMonitor")
return PrerenderCancelledInterface::kGamepadMonitor;
else if (interface_name == "chrome.mojom.SyncEncryptionKeysExtension")
return PrerenderCancelledInterface::kSyncEncryptionKeysExtension;
return PrerenderCancelledInterface::kUnknown;
}
int32_t InterfaceNameHasher(const std::string& interface_name) {
return static_cast<int32_t>(base::HashMetricNameAs32Bits(interface_name));
}
int32_t HeaderMismatchHasher(const std::string& header,
HeaderMismatchType mismatch_type) {
// Throw two bits away to encode the mismatch type.
// {0---30} bits are the encoded hash number.
// {31, 32} bits encode the mismatch type.
static_assert(HeaderMismatchType::kMaxValue == 3u,
"HeaderMismatchType should use 2 bits at most.");
return static_cast<int32_t>(base::HashMetricNameAs32Bits(header) << 2 |
mismatch_type);
}
std::string GenerateHistogramName(const std::string& histogram_base_name,
PrerenderTriggerType trigger_type,
const std::string& embedder_suffix) {
switch (trigger_type) {
case PrerenderTriggerType::kSpeculationRule:
DCHECK(embedder_suffix.empty());
return std::string(histogram_base_name) + ".SpeculationRule";
case PrerenderTriggerType::kEmbedder:
DCHECK(!embedder_suffix.empty());
return std::string(histogram_base_name) + ".Embedder_" + embedder_suffix;
}
NOTREACHED();
}
void ReportHeaderMismatch(const std::string& key,
HeaderMismatchType mismatch_type,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
base::UmaHistogramSparse(
GenerateHistogramName("Prerender.Experimental.ActivationHeadersMismatch",
trigger_type, embedder_histogram_suffix),
HeaderMismatchHasher(base::ToLowerASCII(key), mismatch_type));
}
// Called by MojoBinderPolicyApplier. This function records the Mojo interface
// that causes MojoBinderPolicyApplier to cancel prerendering.
void RecordPrerenderCancelledInterface(
const std::string& interface_name,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
const PrerenderCancelledInterface interface_type =
GetCancelledInterfaceType(interface_name);
base::UmaHistogramEnumeration(
GenerateHistogramName(
"Prerender.Experimental.PrerenderCancelledInterface", trigger_type,
embedder_histogram_suffix),
interface_type);
if (interface_type == PrerenderCancelledInterface::kUnknown) {
// These interfaces can be required by embedders, or not set to kCancel
// expclitly, e.g., channel-associated interfaces. Record these interfaces
// with the sparse histogram to ensure all of them are tracked.
base::UmaHistogramSparse(
GenerateHistogramName(
"Prerender.Experimental.PrerenderCancelledUnknownInterface",
trigger_type, embedder_histogram_suffix),
InterfaceNameHasher(interface_name));
}
}
void RecordPrerenderFinalStatusUma(
PrerenderFinalStatus final_status,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
base::UmaHistogramEnumeration(
GenerateHistogramName("Prerender.Experimental.PrerenderHostFinalStatus",
trigger_type, embedder_histogram_suffix),
final_status);
}
} // namespace
// static
PrerenderCancellationReason
PrerenderCancellationReason::BuildForDisallowActivationState(
uint64_t disallow_activation_reason) {
return PrerenderCancellationReason(
PrerenderFinalStatus::kInactivePageRestriction,
disallow_activation_reason);
}
// static
PrerenderCancellationReason
PrerenderCancellationReason::BuildForMojoBinderPolicy(
const std::string& interface_name) {
return PrerenderCancellationReason(PrerenderFinalStatus::kMojoBinderPolicy,
interface_name);
}
PrerenderCancellationReason::PrerenderCancellationReason(
PrerenderFinalStatus final_status)
: PrerenderCancellationReason(final_status, DetailedReasonVariant()) {}
PrerenderCancellationReason::PrerenderCancellationReason(
PrerenderCancellationReason&& reason) = default;
PrerenderCancellationReason::~PrerenderCancellationReason() = default;
PrerenderCancellationReason::PrerenderCancellationReason(
PrerenderFinalStatus final_status,
DetailedReasonVariant explanation)
: final_status_(final_status), explanation_(std::move(explanation)) {}
void PrerenderCancellationReason::ReportMetrics(
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) const {
switch (final_status_) {
case PrerenderFinalStatus::kInactivePageRestriction:
DCHECK(absl::holds_alternative<uint64_t>(explanation_));
base::UmaHistogramSparse(
GenerateHistogramName("Prerender.CanceledForInactivePageRestriction."
"DisallowActivationReason",
trigger_type, embedder_histogram_suffix),
absl::get<uint64_t>(explanation_));
break;
case PrerenderFinalStatus::kMojoBinderPolicy:
DCHECK(absl::holds_alternative<std::string>(explanation_));
RecordPrerenderCancelledInterface(absl::get<std::string>(explanation_),
trigger_type,
embedder_histogram_suffix);
break;
default:
DCHECK(absl::holds_alternative<absl::monostate>(explanation_));
// Other types need not to report.
break;
}
}
std::string PrerenderCancellationReason::ToDevtoolReasonString() const {
switch (final_status_) {
case PrerenderFinalStatus::kInactivePageRestriction:
DCHECK(absl::holds_alternative<uint64_t>(explanation_));
// TODO(https://crbug.com/1328365): It seems we have to return an integer.
// And devtool has to handle it based on the enum.xml, as the content
// layer cannot know about the enums added by the embedder layer.
return "";
case PrerenderFinalStatus::kMojoBinderPolicy:
DCHECK(absl::holds_alternative<std::string>(explanation_));
return absl::get<std::string>(explanation_);
default:
DCHECK(absl::holds_alternative<absl::monostate>(explanation_));
return "";
}
}
void RecordPrerenderTriggered(ukm::SourceId ukm_id) {
ukm::builders::PrerenderPageLoad(ukm_id).SetTriggeredPrerender(true).Record(
ukm::UkmRecorder::Get());
}
void RecordPrerenderActivationTime(
base::TimeDelta delta,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
base::UmaHistogramTimes(
GenerateHistogramName("Navigation.TimeToActivatePrerender", trigger_type,
embedder_histogram_suffix),
delta);
}
void RecordFailedPrerenderFinalStatus(
const PrerenderCancellationReason& cancellation_reason,
const PrerenderAttributes& attributes) {
DCHECK_NE(cancellation_reason.final_status(),
PrerenderFinalStatus::kActivated);
RecordPrerenderFinalStatusUma(cancellation_reason.final_status(),
attributes.trigger_type,
attributes.embedder_histogram_suffix);
if (attributes.initiator_ukm_id != ukm::kInvalidSourceId) {
// `initiator_ukm_id` must be valid for the speculation rules.
DCHECK_EQ(attributes.trigger_type, PrerenderTriggerType::kSpeculationRule);
ukm::builders::PrerenderPageLoad(attributes.initiator_ukm_id)
.SetFinalStatus(static_cast<int>(cancellation_reason.final_status()))
.Record(ukm::UkmRecorder::Get());
}
// Browser initiated prerendering doesn't report cancellation reasons to the
// DevTools as it doesn't have the initiator frame associated with DevTools
// agents.
if (!attributes.IsBrowserInitiated()) {
auto* ftn = FrameTreeNode::GloballyFindByID(
attributes.initiator_frame_tree_node_id);
DCHECK(ftn);
// TODO(https://crbug.com/1332377): Discuss with devtools to finalize the
// message protocol.
if (attributes.initiator_devtools_navigation_token.has_value()) {
devtools_instrumentation::DidCancelPrerender(
ftn, attributes.prerendering_url,
attributes.initiator_devtools_navigation_token.value(),
cancellation_reason.final_status(),
cancellation_reason.ToDevtoolReasonString());
}
}
}
void ReportSuccessActivation(const PrerenderAttributes& attributes,
ukm::SourceId prerendered_ukm_id) {
RecordPrerenderFinalStatusUma(PrerenderFinalStatus::kActivated,
attributes.trigger_type,
attributes.embedder_histogram_suffix);
if (attributes.initiator_ukm_id != ukm::kInvalidSourceId) {
// `initiator_ukm_id` must be valid only for the speculation rules.
DCHECK_EQ(attributes.trigger_type, PrerenderTriggerType::kSpeculationRule);
ukm::builders::PrerenderPageLoad(attributes.initiator_ukm_id)
.SetFinalStatus(static_cast<int>(PrerenderFinalStatus::kActivated))
.Record(ukm::UkmRecorder::Get());
}
if (prerendered_ukm_id != ukm::kInvalidSourceId) {
ukm::builders::PrerenderPageLoad(prerendered_ukm_id)
.SetFinalStatus(static_cast<int>(PrerenderFinalStatus::kActivated))
.Record(ukm::UkmRecorder::Get());
}
}
void RecordPrerenderActivationNavigationParamsMatch(
PrerenderHost::ActivationNavigationParamsMatch result,
PrerenderTriggerType trigger_type,
const std::string& embedder_suffix) {
base::UmaHistogramEnumeration(
GenerateHistogramName(
"Prerender.Experimental.ActivationNavigationParamsMatch",
trigger_type, embedder_suffix),
result);
}
void RecordPrerenderRedirectionMismatchType(
PrerenderCrossOriginRedirectionMismatch mismatch_type,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
DCHECK_EQ(trigger_type, PrerenderTriggerType::kEmbedder);
base::UmaHistogramEnumeration(
GenerateHistogramName(
"Prerender.Experimental.PrerenderCrossOriginRedirectionMismatch",
trigger_type, embedder_histogram_suffix),
mismatch_type);
}
void RecordPrerenderRedirectionProtocolChange(
PrerenderCrossOriginRedirectionProtocolChange change_type,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
DCHECK_EQ(trigger_type, PrerenderTriggerType::kEmbedder);
base::UmaHistogramEnumeration(
GenerateHistogramName(
"Prerender.Experimental.CrossOriginRedirectionProtocolChange",
trigger_type, embedder_histogram_suffix),
change_type);
}
void AnalyzePrerenderActivationHeader(
net::HttpRequestHeaders potential_activation_headers,
net::HttpRequestHeaders prerender_headers,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix) {
using HeaderPair = net::HttpRequestHeaders::HeaderKeyValuePair;
auto potential_header_dict = base::MakeFlatMap<std::string, std::string>(
potential_activation_headers.GetHeaderVector(), /*comp=default*/ {},
[](HeaderPair x) {
return std::make_pair(base::ToLowerASCII(x.key), x.value);
});
// Masking code for whether the corresponding enum has been removed or not.
// Instead of removing an element from flat_map, we set the mask bit as the
// cost of removal is O(N).
std::vector<bool> removal_set(potential_header_dict.size(), false);
bool detected = false;
for (auto& prerender_header : prerender_headers.GetHeaderVector()) {
std::string key = base::ToLowerASCII(prerender_header.key);
auto potential_it = potential_header_dict.find(key);
if (potential_it == potential_header_dict.end()) {
// The potential activation headers does not contain it.
ReportHeaderMismatch(key, HeaderMismatchType::kMissingInActivation,
trigger_type, embedder_histogram_suffix);
detected = true;
continue;
}
if (!base::EqualsCaseInsensitiveASCII(prerender_header.value,
potential_it->second)) {
ReportHeaderMismatch(key, HeaderMismatchType::kValueMismatch,
trigger_type, embedder_histogram_suffix);
detected = true;
}
// Remove it, since we will report the remaining ones, i.e., the headers
// that are not found in prerendering.
removal_set.at(potential_it - potential_header_dict.begin()) = true;
}
// Iterate over the remaining potential prerendering headers and report it to
// UMA.
for (auto potential_it = potential_header_dict.begin();
potential_it != potential_header_dict.end(); potential_it++) {
if (removal_set.at(potential_it - potential_header_dict.begin())) {
continue;
}
detected = true;
ReportHeaderMismatch(potential_it->first,
HeaderMismatchType::kMissingInPrerendering,
trigger_type, embedder_histogram_suffix);
}
// Use the empty string for the matching case; we use this value for detecting
// bug, that is, comparing strings is wrong.
if (!detected) {
ReportHeaderMismatch("", HeaderMismatchType::kMatch, trigger_type,
embedder_histogram_suffix);
}
}
} // namespace content