| // 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); |
| } |
| } |