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