blob: 0128360301e60f72de9c1eeb5af00252cf0f5b58 [file] [log] [blame] [edit]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "autocomplete_controller_metrics.h"
#include <string>
#include <string_view>
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
namespace {
enum class MetricNameSuffix {
kDone,
kLastChange,
kLastDefaultChange,
kProvider,
kMaxValue = kProvider
};
constexpr std::string_view ToString(MetricNameSuffix name_suffix) {
using enum MetricNameSuffix;
switch (name_suffix) {
case kDone:
return "Done";
case kLastChange:
return "LastChange";
case kLastDefaultChange:
return "LastDefaultChange";
case kProvider:
return "Provider";
}
NOTREACHED();
}
std::string GetMetricName(MetricNameSuffix name_suffix,
const AutocompleteProvider* provider,
std::string_view completion_suffix = "") {
return base::StrCat(
{"Omnibox.AsyncAutocompletionTime2.", ToString(name_suffix),
(provider ? "." : ""), (provider ? provider->GetName() : ""),
(completion_suffix.empty() ? "" : "."), completion_suffix});
}
void LogAsyncAutocompletionTimeMetricsImpl(MetricNameSuffix name_suffix,
const AutocompleteProvider* provider,
bool completed,
base::TimeDelta elapsed_time) {
// This may be called up to 40 times per omnibox key-stroke. Cache the
// histograms in a lookup table keyed by name_suffix + provider_number
// (where provider_number is 0 unless name_suffix == kProvider) so that
// we can avoid having to construct their names and look them up each time.
// The max size of each of the histogram tables.
constexpr int kMaxHistogramSize =
(static_cast<int>(MetricNameSuffix::kMaxValue) +
metrics::OmniboxEventProto_ProviderType_ProviderType_MAX) +
1;
// Validate the histogram lookup parameters:
// * name_suffix is in [0..kMaxValue]
// * `provider` is non-null iff name_suffix == kProvider, and vice versa.
// * if non-null, provider yields a value <= ProviderType_MAX
DCHECK_GE(static_cast<int>(name_suffix), 0);
DCHECK_LE(static_cast<int>(name_suffix),
static_cast<int>(MetricNameSuffix::kMaxValue));
DCHECK_EQ(name_suffix == MetricNameSuffix::kProvider, !!provider);
DCHECK(!provider ||
provider->AsOmniboxEventProviderType() <=
metrics::OmniboxEventProto_ProviderType_ProviderType_MAX);
// Each histogram is at the same index in its respective table.
const int histogram_index =
static_cast<int>(name_suffix) +
(provider ? provider->AsOmniboxEventProviderType() : 0);
// Use the `STATIC_HISTOGRAM_POINTER_GROUP` macro to define a static table of
// atomic histogram pointers which is indexed by `histogram_index`.
//
// I.e., the histograms are ordered as:
// Done, LastChange, DefaultChange, Provider-0, Provider-1, ...
#define STATIC_HISTOGRAM_TIMES_POINTER_GROUP(name, sample) \
STATIC_HISTOGRAM_POINTER_GROUP( \
name, histogram_index, kMaxHistogramSize, \
AddTimeMillisecondsGranularity(sample), \
base::Histogram::FactoryTimeGet( \
name, base::Milliseconds(1), base::Seconds(10), 50, \
base::HistogramBase::kUmaTargetedHistogramFlag))
// These metrics are logged up to about 40 times each per omnibox keystroke.
// `GetMetricName()` is deterministic for any given set of parameters, so each
// histogram name is a run-time constant and a pointer to the corresponding
// histogram object will be cached on first use in a static table.
STATIC_HISTOGRAM_TIMES_POINTER_GROUP(GetMetricName(name_suffix, provider),
elapsed_time);
if (completed) {
STATIC_HISTOGRAM_TIMES_POINTER_GROUP(
GetMetricName(name_suffix, provider, "Completed"), elapsed_time);
} else {
STATIC_HISTOGRAM_TIMES_POINTER_GROUP(
GetMetricName(name_suffix, provider, "Interrupted"), elapsed_time);
}
#undef STATIC_HISTOGRAM_TIMES_POINTER_GROUP
}
inline void LogAsyncAutocompletionTimeMetrics(MetricNameSuffix metric,
bool completed,
base::TimeDelta elapsed_time) {
DCHECK_NE(metric, MetricNameSuffix::kProvider);
LogAsyncAutocompletionTimeMetricsImpl(metric, nullptr, completed,
elapsed_time);
}
inline void LogAsyncAutocompletionTimeMetrics(
const AutocompleteProvider& provider,
base::TimeDelta elapsed_time) {
LogAsyncAutocompletionTimeMetricsImpl(MetricNameSuffix::kProvider, &provider,
provider.done(), elapsed_time);
}
inline bool IsSynchronousUpdate(
AutocompleteController::UpdateType update_type) {
return update_type == AutocompleteController::UpdateType::kSyncPass ||
update_type == AutocompleteController::UpdateType::kSyncPassOnly;
}
} // namespace
AutocompleteControllerMetrics::AutocompleteControllerMetrics(
const AutocompleteController& controller)
: controller_(controller) {}
void AutocompleteControllerMetrics::OnStart() {
OnStop();
logged_finalization_metrics_ = false;
start_time_ = base::TimeTicks::Now();
last_change_time_ = start_time_;
last_default_change_time_ = start_time_;
}
void AutocompleteControllerMetrics::OnNotifyChanged(
std::vector<AutocompleteResult::MatchDedupComparator> last_result,
std::vector<AutocompleteResult::MatchDedupComparator> new_result) {
// Only log metrics for async requests.
if (controller_->input().omit_asynchronous_matches()) {
return;
}
// If results are empty then the omnibox is likely closed, and clearing old
// results won't be user visible. E.g., this occurs when opening a new tab
// while the popup was open.
if (new_result.empty()) {
return;
}
// Log suggestion changes.
bool any_match_changed_or_removed = false;
for (size_t i = 0; i < last_result.size(); ++i) {
// Log changed or removed matches. Don't log for matches appended to the
// bottom since that's less disruptive.
if (i >= new_result.size() || last_result[i] != new_result[i]) {
LogSuggestionChangeIndexMetrics(i);
any_match_changed_or_removed = true;
}
}
LogSuggestionChangeInAnyPositionMetrics(any_match_changed_or_removed);
// Log suggestion finalization times. This handles logging as soon as the
// final update occurs, while `OnStop()` handles the case where the final
// update never occurs because of interruptions.
// E.g., suggestion deletion can call `OnNotifyChanged()` after the controller
// is done and finalization metrics have been logged. They should not be
// logged again.
if (controller_->last_update_type() ==
AutocompleteController::UpdateType::kMatchDeletion) {
return;
}
const bool any_match_changed_or_removed_or_added =
any_match_changed_or_removed || last_result.size() != new_result.size();
const bool default_match_changed_or_removed_or_added =
last_result.empty() || last_result[0] != new_result[0];
if (any_match_changed_or_removed_or_added) {
last_change_time_ = base::TimeTicks::Now();
}
if (default_match_changed_or_removed_or_added) {
DCHECK(any_match_changed_or_removed_or_added);
last_default_change_time_ = last_change_time_;
}
// It's common to have multiple async updates per input. Only log the final
// update.
// TODO(crbug.com/364303536): `logged_finalization_metrics_` should be
// guaranteed false here (hence the DCHECK in
// `LogSuggestionFinalizationMetrics()`. But because of a temporary band-aid
// to allow history embedding answers and unscoped extension suggestions to
// ignore the stop timer, we need to check it anyways.
if (controller_->done() && !logged_finalization_metrics_) {
LogSuggestionFinalizationMetrics();
}
}
void AutocompleteControllerMetrics::OnProviderUpdate(
const AutocompleteProvider& provider) const {
// Only log metrics for async requests. This will likely never happen, since
// `OnProviderUpdate()` is only called by async providers (but not necessarily
// async'ly, see the comments in `AutocompleteController::OnProviderUpdate`).
if (controller_->input().omit_asynchronous_matches()) {
return;
}
// Some async providers may produce multiple updates. Only log the final async
// update.
if (provider.done()) {
LogProviderTimeMetrics(provider);
}
}
void AutocompleteControllerMetrics::OnStop() {
// Only log metrics for async requests.
if (controller_->input().omit_asynchronous_matches()) {
return;
}
// Done providers should already be logged by `OnProviderUpdate()`.
for (const auto& provider : controller_->providers()) {
if (!provider->done()) {
LogProviderTimeMetrics(*provider);
}
}
// If the controller is done, `OnNotifyChanged()` should have already
// logged finalization metrics. This case, i.e. `OnStop()` invoked even
// though the controller is done, is possible because `OnStart()` calls
// `OnStop()`.
// TODO(crbug.com/364303536): `logged_finalization_metrics_` should be
// guaranteed false here (hence the DCHECK in
// `LogSuggestionFinalizationMetrics()`. But because of a temporary
// bandaid to allow history embedding answers and unscoped extension
// answers to ignore the stop timer, we need to check it anyways.
if (!controller_->done() && !logged_finalization_metrics_) {
LogSuggestionFinalizationMetrics();
}
}
void AutocompleteControllerMetrics::LogSuggestionFinalizationMetrics() {
// Finalization metrics should be logged once only, either when all
// async providers complete or they're interrupted before completion.
#if BUILDFLAG(IS_IOS)
// iOS is weird in that it sometimes calls `InjectAdHocMatch()` when the user
// selects a suggestion, thus changing the results when autocompletion is done
// and suggestions should be stable.
if (logged_finalization_metrics_)
return;
#endif
DCHECK(!logged_finalization_metrics_)
<< "last_update_type: "
<< AutocompleteController::UpdateTypeToDebugString(
controller_->last_update_type());
logged_finalization_metrics_ = true;
const auto done_elapsed_time = base::TimeTicks::Now() - start_time_;
const auto last_change_elapsed_time = last_change_time_ - start_time_;
const auto last_default_change_elapsed_time =
last_default_change_time_ - start_time_;
const bool is_completed = controller_->done();
using enum MetricNameSuffix;
LogAsyncAutocompletionTimeMetrics(kDone, is_completed, done_elapsed_time);
LogAsyncAutocompletionTimeMetrics(kLastChange, is_completed,
last_change_elapsed_time);
LogAsyncAutocompletionTimeMetrics(kLastDefaultChange, is_completed,
last_default_change_elapsed_time);
}
void AutocompleteControllerMetrics::LogProviderTimeMetrics(
const AutocompleteProvider& provider) const {
const auto elapsed_time = base::TimeTicks::Now() - start_time_;
LogAsyncAutocompletionTimeMetrics(provider, elapsed_time);
}
void AutocompleteControllerMetrics::LogSuggestionChangeIndexMetrics(
size_t change_index) const {
// These metrics are logged up to about 50 times per omnibox keystroke, so use
// the UMA macros (which cache the histogram pointer) for efficiency. All of
// the histogram names used in this function are run-time constants.
static constexpr char kName[] = "Omnibox.MatchStability2.MatchChangeIndex";
constexpr size_t max = AutocompleteResult::kMaxAutocompletePositionValue;
UMA_HISTOGRAM_EXACT_LINEAR(kName, change_index, max);
if (IsSynchronousUpdate(controller_->last_update_type())) {
UMA_HISTOGRAM_EXACT_LINEAR(base::StrCat({kName, ".CrossInput"}),
change_index, max);
} else {
UMA_HISTOGRAM_EXACT_LINEAR(base::StrCat({kName, ".Async"}), change_index,
max);
}
}
void AutocompleteControllerMetrics::LogSuggestionChangeInAnyPositionMetrics(
bool changed) const {
// These metrics are logged up to about 5 times per omnibox keystroke, so use
// the UMA macros (which cache the histogram pointer) for efficiency. All of
// the histogram names used in this function are run-time constants.
static constexpr char kName[] =
"Omnibox.MatchStability2.MatchChangeInAnyPosition";
UMA_HISTOGRAM_BOOLEAN(kName, changed);
if (IsSynchronousUpdate(controller_->last_update_type())) {
UMA_HISTOGRAM_BOOLEAN(base::StrCat({kName, ".CrossInput"}), changed);
} else {
UMA_HISTOGRAM_BOOLEAN(base::StrCat({kName, ".Async"}), changed);
}
}