Home | History | Annotate | Download | only in omnibox
      1 // Copyright 2014 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/omnibox/omnibox_field_trial.h"
      6 
      7 #include <cmath>
      8 #include <string>
      9 
     10 #include "base/command_line.h"
     11 #include "base/metrics/field_trial.h"
     12 #include "base/strings/string_number_conversions.h"
     13 #include "base/strings/string_split.h"
     14 #include "base/strings/string_util.h"
     15 #include "base/strings/stringprintf.h"
     16 #include "base/time/time.h"
     17 #include "components/metrics/proto/omnibox_event.pb.h"
     18 #include "components/omnibox/omnibox_switches.h"
     19 #include "components/search/search.h"
     20 #include "components/variations/active_field_trials.h"
     21 #include "components/variations/metrics_util.h"
     22 #include "components/variations/variations_associated_data.h"
     23 
     24 using metrics::OmniboxEventProto;
     25 
     26 namespace {
     27 
     28 typedef std::map<std::string, std::string> VariationParams;
     29 typedef HUPScoringParams::ScoreBuckets ScoreBuckets;
     30 
     31 // Field trial names.
     32 const char kStopTimerFieldTrialName[] = "OmniboxStopTimer";
     33 
     34 // The autocomplete dynamic field trial name prefix.  Each field trial is
     35 // configured dynamically and is retrieved automatically by Chrome during
     36 // the startup.
     37 const char kAutocompleteDynamicFieldTrialPrefix[] = "AutocompleteDynamicTrial_";
     38 // The maximum number of the autocomplete dynamic field trials (aka layers).
     39 const int kMaxAutocompleteDynamicFieldTrials = 5;
     40 
     41 
     42 // Concatenates the autocomplete dynamic field trial prefix with a field trial
     43 // ID to form a complete autocomplete field trial name.
     44 std::string DynamicFieldTrialName(int id) {
     45   return base::StringPrintf("%s%d", kAutocompleteDynamicFieldTrialPrefix, id);
     46 }
     47 
     48 void InitializeScoreBuckets(const VariationParams& params,
     49                             const char* relevance_cap_param,
     50                             const char* half_life_param,
     51                             const char* score_buckets_param,
     52                             ScoreBuckets* score_buckets) {
     53   VariationParams::const_iterator it = params.find(relevance_cap_param);
     54   if (it != params.end()) {
     55     int relevance_cap;
     56     if (base::StringToInt(it->second, &relevance_cap))
     57       score_buckets->set_relevance_cap(relevance_cap);
     58   }
     59 
     60   it = params.find(half_life_param);
     61   if (it != params.end()) {
     62     int half_life_days;
     63     if (base::StringToInt(it->second, &half_life_days))
     64       score_buckets->set_half_life_days(half_life_days);
     65   }
     66 
     67   it = params.find(score_buckets_param);
     68   if (it != params.end()) {
     69     // The value of the score bucket is a comma-separated list of
     70     // {DecayedCount + ":" + MaxRelevance}.
     71     base::StringPairs kv_pairs;
     72     if (base::SplitStringIntoKeyValuePairs(it->second, ':', ',', &kv_pairs)) {
     73       for (base::StringPairs::const_iterator it = kv_pairs.begin();
     74            it != kv_pairs.end(); ++it) {
     75         ScoreBuckets::CountMaxRelevance bucket;
     76         base::StringToDouble(it->first, &bucket.first);
     77         base::StringToInt(it->second, &bucket.second);
     78         score_buckets->buckets().push_back(bucket);
     79       }
     80       std::sort(score_buckets->buckets().begin(),
     81                 score_buckets->buckets().end(),
     82                 std::greater<ScoreBuckets::CountMaxRelevance>());
     83     }
     84   }
     85 }
     86 
     87 }  // namespace
     88 
     89 HUPScoringParams::ScoreBuckets::ScoreBuckets()
     90     : relevance_cap_(-1),
     91       half_life_days_(-1) {
     92 }
     93 
     94 HUPScoringParams::ScoreBuckets::~ScoreBuckets() {
     95 }
     96 
     97 double HUPScoringParams::ScoreBuckets::HalfLifeTimeDecay(
     98     const base::TimeDelta& elapsed_time) const {
     99   double time_ms;
    100   if ((half_life_days_ <= 0) ||
    101       ((time_ms = elapsed_time.InMillisecondsF()) <= 0))
    102     return 1.0;
    103 
    104   const double half_life_intervals =
    105       time_ms / base::TimeDelta::FromDays(half_life_days_).InMillisecondsF();
    106   return pow(2.0, -half_life_intervals);
    107 }
    108 
    109 void OmniboxFieldTrial::ActivateDynamicTrials() {
    110   // Initialize all autocomplete dynamic field trials.  This method may be
    111   // called multiple times.
    112   for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i)
    113     base::FieldTrialList::FindValue(DynamicFieldTrialName(i));
    114 }
    115 
    116 int OmniboxFieldTrial::GetDisabledProviderTypes() {
    117   // Make sure that Autocomplete dynamic field trials are activated.  It's OK to
    118   // call this method multiple times.
    119   ActivateDynamicTrials();
    120 
    121   // Look for group names in form of "DisabledProviders_<mask>" where "mask"
    122   // is a bitmap of disabled provider types (AutocompleteProvider::Type).
    123   int provider_types = 0;
    124   for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) {
    125     std::string group_name = base::FieldTrialList::FindFullName(
    126         DynamicFieldTrialName(i));
    127     const char kDisabledProviders[] = "DisabledProviders_";
    128     if (!StartsWithASCII(group_name, kDisabledProviders, true))
    129       continue;
    130     int types = 0;
    131     if (!base::StringToInt(base::StringPiece(
    132             group_name.substr(strlen(kDisabledProviders))), &types))
    133       continue;
    134     provider_types |= types;
    135   }
    136   return provider_types;
    137 }
    138 
    139 void OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(
    140     std::vector<uint32>* field_trial_hashes) {
    141   field_trial_hashes->clear();
    142   for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) {
    143     const std::string& trial_name = DynamicFieldTrialName(i);
    144     if (base::FieldTrialList::TrialExists(trial_name))
    145       field_trial_hashes->push_back(metrics::HashName(trial_name));
    146   }
    147   if (base::FieldTrialList::TrialExists(kBundledExperimentFieldTrialName)) {
    148     field_trial_hashes->push_back(
    149         metrics::HashName(kBundledExperimentFieldTrialName));
    150   }
    151 }
    152 
    153 base::TimeDelta OmniboxFieldTrial::StopTimerFieldTrialDuration() {
    154   int stop_timer_ms;
    155   if (base::StringToInt(
    156       base::FieldTrialList::FindFullName(kStopTimerFieldTrialName),
    157           &stop_timer_ms))
    158     return base::TimeDelta::FromMilliseconds(stop_timer_ms);
    159   return base::TimeDelta::FromMilliseconds(1500);
    160 }
    161 
    162 bool OmniboxFieldTrial::InZeroSuggestFieldTrial() {
    163   if (variations::GetVariationParamValue(
    164           kBundledExperimentFieldTrialName, kZeroSuggestRule) == "true")
    165     return true;
    166   if (variations::GetVariationParamValue(
    167           kBundledExperimentFieldTrialName, kZeroSuggestRule) == "false")
    168     return false;
    169 #if defined(OS_WIN) || defined(OS_CHROMEOS) || defined(OS_LINUX) || \
    170     (defined(OS_MACOSX) && !defined(OS_IOS))
    171   return true;
    172 #else
    173   return false;
    174 #endif
    175 }
    176 
    177 bool OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() {
    178   return variations::GetVariationParamValue(
    179       kBundledExperimentFieldTrialName,
    180       kZeroSuggestVariantRule) == "MostVisited";
    181 }
    182 
    183 bool OmniboxFieldTrial::InZeroSuggestAfterTypingFieldTrial() {
    184   return variations::GetVariationParamValue(
    185       kBundledExperimentFieldTrialName,
    186       kZeroSuggestVariantRule) == "AfterTyping";
    187 }
    188 
    189 bool OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial() {
    190   return variations::GetVariationParamValue(
    191       kBundledExperimentFieldTrialName,
    192       kZeroSuggestVariantRule) == "Personalized";
    193 }
    194 
    195 bool OmniboxFieldTrial::ShortcutsScoringMaxRelevance(
    196     OmniboxEventProto::PageClassification current_page_classification,
    197     int* max_relevance) {
    198   // The value of the rule is a string that encodes an integer containing
    199   // the max relevance.
    200   const std::string& max_relevance_str =
    201       OmniboxFieldTrial::GetValueForRuleInContext(
    202           kShortcutsScoringMaxRelevanceRule, current_page_classification);
    203   if (max_relevance_str.empty())
    204     return false;
    205   if (!base::StringToInt(max_relevance_str, max_relevance))
    206     return false;
    207   return true;
    208 }
    209 
    210 bool OmniboxFieldTrial::SearchHistoryPreventInlining(
    211     OmniboxEventProto::PageClassification current_page_classification) {
    212   return OmniboxFieldTrial::GetValueForRuleInContext(
    213       kSearchHistoryRule, current_page_classification) == "PreventInlining";
    214 }
    215 
    216 bool OmniboxFieldTrial::SearchHistoryDisable(
    217     OmniboxEventProto::PageClassification current_page_classification) {
    218   return OmniboxFieldTrial::GetValueForRuleInContext(
    219       kSearchHistoryRule, current_page_classification) == "Disable";
    220 }
    221 
    222 void OmniboxFieldTrial::GetDemotionsByType(
    223     OmniboxEventProto::PageClassification current_page_classification,
    224     DemotionMultipliers* demotions_by_type) {
    225   demotions_by_type->clear();
    226   std::string demotion_rule = OmniboxFieldTrial::GetValueForRuleInContext(
    227       kDemoteByTypeRule, current_page_classification);
    228   // If there is no demotion rule for this context, then use the default
    229   // value for that context.  At the moment the default value is non-empty
    230   // only for the fakebox-focus context.
    231   if (demotion_rule.empty() &&
    232       (current_page_classification ==
    233        OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS))
    234     demotion_rule = "1:61,2:61,3:61,4:61,16:61";
    235 
    236   // The value of the DemoteByType rule is a comma-separated list of
    237   // {ResultType + ":" + Number} where ResultType is an AutocompleteMatchType::
    238   // Type enum represented as an integer and Number is an integer number
    239   // between 0 and 100 inclusive.   Relevance scores of matches of that result
    240   // type are multiplied by Number / 100.  100 means no change.
    241   base::StringPairs kv_pairs;
    242   if (base::SplitStringIntoKeyValuePairs(demotion_rule, ':', ',', &kv_pairs)) {
    243     for (base::StringPairs::const_iterator it = kv_pairs.begin();
    244          it != kv_pairs.end(); ++it) {
    245       // This is a best-effort conversion; we trust the hand-crafted parameters
    246       // downloaded from the server to be perfect.  There's no need to handle
    247       // errors smartly.
    248       int k, v;
    249       base::StringToInt(it->first, &k);
    250       base::StringToInt(it->second, &v);
    251       (*demotions_by_type)[static_cast<AutocompleteMatchType::Type>(k)] =
    252           static_cast<float>(v) / 100.0f;
    253     }
    254   }
    255 }
    256 
    257 void OmniboxFieldTrial::GetExperimentalHUPScoringParams(
    258     HUPScoringParams* scoring_params) {
    259   scoring_params->experimental_scoring_enabled = false;
    260 
    261   VariationParams params;
    262   if (!variations::GetVariationParams(kBundledExperimentFieldTrialName,
    263                                       &params))
    264     return;
    265 
    266   VariationParams::const_iterator it = params.find(kHUPNewScoringEnabledParam);
    267   if (it != params.end()) {
    268     int enabled = 0;
    269     if (base::StringToInt(it->second, &enabled))
    270       scoring_params->experimental_scoring_enabled = (enabled != 0);
    271   }
    272 
    273   InitializeScoreBuckets(params, kHUPNewScoringTypedCountRelevanceCapParam,
    274       kHUPNewScoringTypedCountHalfLifeTimeParam,
    275       kHUPNewScoringTypedCountScoreBucketsParam,
    276       &scoring_params->typed_count_buckets);
    277   InitializeScoreBuckets(params, kHUPNewScoringVisitedCountRelevanceCapParam,
    278       kHUPNewScoringVisitedCountHalfLifeTimeParam,
    279       kHUPNewScoringVisitedCountScoreBucketsParam,
    280       &scoring_params->visited_count_buckets);
    281 }
    282 
    283 int OmniboxFieldTrial::HQPBookmarkValue() {
    284   std::string bookmark_value_str =
    285       variations::GetVariationParamValue(kBundledExperimentFieldTrialName,
    286                                          kHQPBookmarkValueRule);
    287   if (bookmark_value_str.empty())
    288     return 10;
    289   // This is a best-effort conversion; we trust the hand-crafted parameters
    290   // downloaded from the server to be perfect.  There's no need for handle
    291   // errors smartly.
    292   int bookmark_value;
    293   base::StringToInt(bookmark_value_str, &bookmark_value);
    294   return bookmark_value;
    295 }
    296 
    297 bool OmniboxFieldTrial::HQPAllowMatchInTLDValue() {
    298   return variations::GetVariationParamValue(
    299       kBundledExperimentFieldTrialName,
    300       kHQPAllowMatchInTLDRule) == "true";
    301 }
    302 
    303 bool OmniboxFieldTrial::HQPAllowMatchInSchemeValue() {
    304   return variations::GetVariationParamValue(
    305       kBundledExperimentFieldTrialName,
    306       kHQPAllowMatchInSchemeRule) == "true";
    307 }
    308 
    309 bool OmniboxFieldTrial::DisableInlining() {
    310   return variations::GetVariationParamValue(
    311       kBundledExperimentFieldTrialName,
    312       kDisableInliningRule) == "true";
    313 }
    314 
    315 bool OmniboxFieldTrial::EnableAnswersInSuggest() {
    316   const CommandLine* cl = CommandLine::ForCurrentProcess();
    317   if (cl->HasSwitch(switches::kDisableAnswersInSuggest))
    318     return false;
    319   if (cl->HasSwitch(switches::kEnableAnswersInSuggest))
    320     return true;
    321 
    322   return variations::GetVariationParamValue(
    323       kBundledExperimentFieldTrialName,
    324       kAnswersInSuggestRule) == "true";
    325 }
    326 
    327 bool OmniboxFieldTrial::AddUWYTMatchEvenIfPromotedURLs() {
    328   return variations::GetVariationParamValue(
    329       kBundledExperimentFieldTrialName,
    330       kAddUWYTMatchEvenIfPromotedURLsRule) == "true";
    331 }
    332 
    333 bool OmniboxFieldTrial::DisplayHintTextWhenPossible() {
    334   return variations::GetVariationParamValue(
    335       kBundledExperimentFieldTrialName,
    336       kDisplayHintTextWhenPossibleRule) == "true";
    337 }
    338 
    339 const char OmniboxFieldTrial::kBundledExperimentFieldTrialName[] =
    340     "OmniboxBundledExperimentV1";
    341 const char OmniboxFieldTrial::kShortcutsScoringMaxRelevanceRule[] =
    342     "ShortcutsScoringMaxRelevance";
    343 const char OmniboxFieldTrial::kSearchHistoryRule[] = "SearchHistory";
    344 const char OmniboxFieldTrial::kDemoteByTypeRule[] = "DemoteByType";
    345 const char OmniboxFieldTrial::kHQPBookmarkValueRule[] =
    346     "HQPBookmarkValue";
    347 const char OmniboxFieldTrial::kHQPAllowMatchInTLDRule[] = "HQPAllowMatchInTLD";
    348 const char OmniboxFieldTrial::kHQPAllowMatchInSchemeRule[] =
    349     "HQPAllowMatchInScheme";
    350 const char OmniboxFieldTrial::kZeroSuggestRule[] = "ZeroSuggest";
    351 const char OmniboxFieldTrial::kZeroSuggestVariantRule[] = "ZeroSuggestVariant";
    352 const char OmniboxFieldTrial::kDisableInliningRule[] = "DisableInlining";
    353 const char OmniboxFieldTrial::kAnswersInSuggestRule[] = "AnswersInSuggest";
    354 const char OmniboxFieldTrial::kAddUWYTMatchEvenIfPromotedURLsRule[] =
    355     "AddUWYTMatchEvenIfPromotedURLs";
    356 const char OmniboxFieldTrial::kDisplayHintTextWhenPossibleRule[] =
    357     "DisplayHintTextWhenPossible";
    358 
    359 const char OmniboxFieldTrial::kHUPNewScoringEnabledParam[] =
    360     "HUPExperimentalScoringEnabled";
    361 const char OmniboxFieldTrial::kHUPNewScoringTypedCountRelevanceCapParam[] =
    362     "TypedCountRelevanceCap";
    363 const char OmniboxFieldTrial::kHUPNewScoringTypedCountHalfLifeTimeParam[] =
    364     "TypedCountHalfLifeTime";
    365 const char OmniboxFieldTrial::kHUPNewScoringTypedCountScoreBucketsParam[] =
    366     "TypedCountScoreBuckets";
    367 const char OmniboxFieldTrial::kHUPNewScoringVisitedCountRelevanceCapParam[] =
    368     "VisitedCountRelevanceCap";
    369 const char OmniboxFieldTrial::kHUPNewScoringVisitedCountHalfLifeTimeParam[] =
    370     "VisitedCountHalfLifeTime";
    371 const char OmniboxFieldTrial::kHUPNewScoringVisitedCountScoreBucketsParam[] =
    372     "VisitedCountScoreBuckets";
    373 
    374 // Background and implementation details:
    375 //
    376 // Each experiment group in any field trial can come with an optional set of
    377 // parameters (key-value pairs).  In the bundled omnibox experiment
    378 // (kBundledExperimentFieldTrialName), each experiment group comes with a
    379 // list of parameters in the form:
    380 //   key=<Rule>:
    381 //       <OmniboxEventProto::PageClassification (as an int)>:
    382 //       <whether Instant Extended is enabled (as a 1 or 0)>
    383 //     (note that there are no linebreaks in keys; this format is for
    384 //      presentation only>
    385 //   value=<arbitrary string>
    386 // Both the OmniboxEventProto::PageClassification and the Instant Extended
    387 // entries can be "*", which means this rule applies for all values of the
    388 // matching portion of the context.
    389 // One example parameter is
    390 //   key=SearchHistory:6:1
    391 //   value=PreventInlining
    392 // This means in page classification context 6 (a search result page doing
    393 // search term replacement) with Instant Extended enabled, the SearchHistory
    394 // experiment should PreventInlining.
    395 //
    396 // When an exact match to the rule in the current context is missing, we
    397 // give preference to a wildcard rule that matches the instant extended
    398 // context over a wildcard rule that matches the page classification
    399 // context.  Hopefully, though, users will write their field trial configs
    400 // so as not to rely on this fall back order.
    401 //
    402 // In short, this function tries to find the value associated with key
    403 // |rule|:|page_classification|:|instant_extended|, failing that it looks up
    404 // |rule|:*:|instant_extended|, failing that it looks up
    405 // |rule|:|page_classification|:*, failing that it looks up |rule|:*:*,
    406 // and failing that it returns the empty string.
    407 std::string OmniboxFieldTrial::GetValueForRuleInContext(
    408     const std::string& rule,
    409     OmniboxEventProto::PageClassification page_classification) {
    410   VariationParams params;
    411   if (!variations::GetVariationParams(kBundledExperimentFieldTrialName,
    412                                       &params)) {
    413     return std::string();
    414   }
    415   const std::string page_classification_str =
    416       base::IntToString(static_cast<int>(page_classification));
    417   const std::string instant_extended =
    418       chrome::IsInstantExtendedAPIEnabled() ? "1" : "0";
    419   // Look up rule in this exact context.
    420   VariationParams::const_iterator it = params.find(
    421       rule + ":" + page_classification_str + ":" + instant_extended);
    422   if (it != params.end())
    423     return it->second;
    424   // Fall back to the global page classification context.
    425   it = params.find(rule + ":*:" + instant_extended);
    426   if (it != params.end())
    427     return it->second;
    428   // Fall back to the global instant extended context.
    429   it = params.find(rule + ":" + page_classification_str + ":*");
    430   if (it != params.end())
    431     return it->second;
    432   // Look up rule in the global context.
    433   it = params.find(rule + ":*:*");
    434   return (it != params.end()) ? it->second : std::string();
    435 }
    436