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 ¶ms)) 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 ¶ms)) { 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