OLD | NEW |
| (Empty) |
1 // Copyright 2013 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/metrics/variations/variations_seed_processor.h" | |
6 | |
7 #include <map> | |
8 #include <set> | |
9 #include <vector> | |
10 | |
11 #include "base/command_line.h" | |
12 #include "base/metrics/field_trial.h" | |
13 #include "base/stl_util.h" | |
14 #include "base/version.h" | |
15 #include "chrome/browser/metrics/proto/trials_seed.pb.h" | |
16 #include "chrome/common/metrics/variations/variations_associated_data.h" | |
17 | |
18 namespace chrome_variations { | |
19 | |
20 namespace { | |
21 | |
22 Study_Platform GetCurrentPlatform() { | |
23 #if defined(OS_WIN) | |
24 return Study_Platform_PLATFORM_WINDOWS; | |
25 #elif defined(OS_IOS) | |
26 return Study_Platform_PLATFORM_IOS; | |
27 #elif defined(OS_MACOSX) | |
28 return Study_Platform_PLATFORM_MAC; | |
29 #elif defined(OS_CHROMEOS) | |
30 return Study_Platform_PLATFORM_CHROMEOS; | |
31 #elif defined(OS_ANDROID) | |
32 return Study_Platform_PLATFORM_ANDROID; | |
33 #elif defined(OS_LINUX) || defined(OS_BSD) || defined(OS_SOLARIS) | |
34 // Default BSD and SOLARIS to Linux to not break those builds, although these | |
35 // platforms are not officially supported by Chrome. | |
36 return Study_Platform_PLATFORM_LINUX; | |
37 #else | |
38 #error Unknown platform | |
39 #endif | |
40 } | |
41 | |
42 // Converts |date_time| in Study date format to base::Time. | |
43 base::Time ConvertStudyDateToBaseTime(int64 date_time) { | |
44 return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time); | |
45 } | |
46 | |
47 } // namespace | |
48 | |
49 VariationsSeedProcessor::VariationsSeedProcessor() { | |
50 } | |
51 | |
52 VariationsSeedProcessor::~VariationsSeedProcessor() { | |
53 } | |
54 | |
55 void VariationsSeedProcessor::CreateTrialsFromSeed( | |
56 const TrialsSeed& seed, | |
57 const std::string& locale, | |
58 const base::Time& reference_date, | |
59 const base::Version& version, | |
60 Study_Channel channel) { | |
61 DCHECK(version.IsValid()); | |
62 | |
63 // Add expired studies (in a disabled state) only after all the non-expired | |
64 // studies have been added (and do not add an expired study if a corresponding | |
65 // non-expired study got added). This way, if there's both an expired and a | |
66 // non-expired study that applies, the non-expired study takes priority. | |
67 std::set<std::string> created_studies; | |
68 std::vector<const Study*> expired_studies; | |
69 | |
70 for (int i = 0; i < seed.study_size(); ++i) { | |
71 const Study& study = seed.study(i); | |
72 if (!ShouldAddStudy(study, locale, reference_date, version, channel)) | |
73 continue; | |
74 | |
75 if (IsStudyExpired(study, reference_date)) { | |
76 expired_studies.push_back(&study); | |
77 } else { | |
78 CreateTrialFromStudy(study, false); | |
79 created_studies.insert(study.name()); | |
80 } | |
81 } | |
82 | |
83 for (size_t i = 0; i < expired_studies.size(); ++i) { | |
84 if (!ContainsKey(created_studies, expired_studies[i]->name())) | |
85 CreateTrialFromStudy(*expired_studies[i], true); | |
86 } | |
87 } | |
88 | |
89 bool VariationsSeedProcessor::CheckStudyChannel(const Study_Filter& filter, | |
90 Study_Channel channel) { | |
91 // An empty channel list matches all channels. | |
92 if (filter.channel_size() == 0) | |
93 return true; | |
94 | |
95 for (int i = 0; i < filter.channel_size(); ++i) { | |
96 if (filter.channel(i) == channel) | |
97 return true; | |
98 } | |
99 return false; | |
100 } | |
101 | |
102 bool VariationsSeedProcessor::CheckStudyLocale( | |
103 const Study_Filter& filter, | |
104 const std::string& locale) { | |
105 // An empty locale list matches all locales. | |
106 if (filter.locale_size() == 0) | |
107 return true; | |
108 | |
109 for (int i = 0; i < filter.locale_size(); ++i) { | |
110 if (filter.locale(i) == locale) | |
111 return true; | |
112 } | |
113 return false; | |
114 } | |
115 | |
116 bool VariationsSeedProcessor::CheckStudyPlatform( | |
117 const Study_Filter& filter, | |
118 Study_Platform platform) { | |
119 // An empty platform list matches all platforms. | |
120 if (filter.platform_size() == 0) | |
121 return true; | |
122 | |
123 for (int i = 0; i < filter.platform_size(); ++i) { | |
124 if (filter.platform(i) == platform) | |
125 return true; | |
126 } | |
127 return false; | |
128 } | |
129 | |
130 bool VariationsSeedProcessor::CheckStudyStartDate( | |
131 const Study_Filter& filter, | |
132 const base::Time& date_time) { | |
133 if (filter.has_start_date()) { | |
134 const base::Time start_date = | |
135 ConvertStudyDateToBaseTime(filter.start_date()); | |
136 return date_time >= start_date; | |
137 } | |
138 | |
139 return true; | |
140 } | |
141 | |
142 bool VariationsSeedProcessor::CheckStudyVersion( | |
143 const Study_Filter& filter, | |
144 const base::Version& version) { | |
145 if (filter.has_min_version()) { | |
146 if (version.CompareToWildcardString(filter.min_version()) < 0) | |
147 return false; | |
148 } | |
149 | |
150 if (filter.has_max_version()) { | |
151 if (version.CompareToWildcardString(filter.max_version()) > 0) | |
152 return false; | |
153 } | |
154 | |
155 return true; | |
156 } | |
157 | |
158 void VariationsSeedProcessor::CreateTrialFromStudy(const Study& study, | |
159 bool is_expired) { | |
160 base::FieldTrial::Probability total_probability = 0; | |
161 if (!ValidateStudyAndComputeTotalProbability(study, &total_probability)) | |
162 return; | |
163 | |
164 // Check if any experiments need to be forced due to a command line | |
165 // flag. Force the first experiment with an existing flag. | |
166 CommandLine* command_line = CommandLine::ForCurrentProcess(); | |
167 for (int i = 0; i < study.experiment_size(); ++i) { | |
168 const Study_Experiment& experiment = study.experiment(i); | |
169 if (experiment.has_forcing_flag() && | |
170 command_line->HasSwitch(experiment.forcing_flag())) { | |
171 base::FieldTrialList::CreateFieldTrial(study.name(), experiment.name()); | |
172 DVLOG(1) << "Trial " << study.name() << " forced by flag: " | |
173 << experiment.forcing_flag(); | |
174 return; | |
175 } | |
176 } | |
177 | |
178 uint32 randomization_seed = 0; | |
179 base::FieldTrial::RandomizationType randomization_type = | |
180 base::FieldTrial::SESSION_RANDOMIZED; | |
181 if (study.has_consistency() && | |
182 study.consistency() == Study_Consistency_PERMANENT) { | |
183 randomization_type = base::FieldTrial::ONE_TIME_RANDOMIZED; | |
184 if (study.has_randomization_seed()) | |
185 randomization_seed = study.randomization_seed(); | |
186 } | |
187 | |
188 // The trial is created without specifying an expiration date because the | |
189 // expiration check in field_trial.cc is based on the build date. Instead, | |
190 // the expiration check using |reference_date| is done explicitly below. | |
191 scoped_refptr<base::FieldTrial> trial( | |
192 base::FieldTrialList::FactoryGetFieldTrialWithRandomizationSeed( | |
193 study.name(), total_probability, study.default_experiment_name(), | |
194 base::FieldTrialList::kNoExpirationYear, 1, 1, randomization_type, | |
195 randomization_seed, NULL)); | |
196 | |
197 for (int i = 0; i < study.experiment_size(); ++i) { | |
198 const Study_Experiment& experiment = study.experiment(i); | |
199 | |
200 std::map<std::string, std::string> params; | |
201 for (int j = 0; j < experiment.param_size(); j++) { | |
202 if (experiment.param(j).has_name() && experiment.param(j).has_value()) | |
203 params[experiment.param(j).name()] = experiment.param(j).value(); | |
204 } | |
205 if (!params.empty()) | |
206 AssociateVariationParams(study.name(), experiment.name(), params); | |
207 | |
208 // Groups with flags can't be selected randomly, so we don't add them to | |
209 // the field trial. | |
210 if (experiment.has_forcing_flag()) | |
211 continue; | |
212 | |
213 if (experiment.name() != study.default_experiment_name()) | |
214 trial->AppendGroup(experiment.name(), experiment.probability_weight()); | |
215 | |
216 if (experiment.has_google_web_experiment_id()) { | |
217 const VariationID variation_id = | |
218 static_cast<VariationID>(experiment.google_web_experiment_id()); | |
219 AssociateGoogleVariationIDForce(GOOGLE_WEB_PROPERTIES, | |
220 study.name(), | |
221 experiment.name(), | |
222 variation_id); | |
223 } | |
224 if (experiment.has_google_update_experiment_id()) { | |
225 const VariationID variation_id = | |
226 static_cast<VariationID>(experiment.google_update_experiment_id()); | |
227 AssociateGoogleVariationIDForce(GOOGLE_UPDATE_SERVICE, | |
228 study.name(), | |
229 experiment.name(), | |
230 variation_id); | |
231 } | |
232 } | |
233 | |
234 trial->SetForced(); | |
235 if (is_expired) | |
236 trial->Disable(); | |
237 } | |
238 | |
239 bool VariationsSeedProcessor::IsStudyExpired(const Study& study, | |
240 const base::Time& date_time) { | |
241 if (study.has_expiry_date()) { | |
242 const base::Time expiry_date = | |
243 ConvertStudyDateToBaseTime(study.expiry_date()); | |
244 return date_time >= expiry_date; | |
245 } | |
246 | |
247 return false; | |
248 } | |
249 | |
250 bool VariationsSeedProcessor::ShouldAddStudy( | |
251 const Study& study, | |
252 const std::string& locale, | |
253 const base::Time& reference_date, | |
254 const base::Version& version, | |
255 Study_Channel channel) { | |
256 if (study.has_filter()) { | |
257 if (!CheckStudyChannel(study.filter(), channel)) { | |
258 DVLOG(1) << "Filtered out study " << study.name() << " due to channel."; | |
259 return false; | |
260 } | |
261 | |
262 if (!CheckStudyLocale(study.filter(), locale)) { | |
263 DVLOG(1) << "Filtered out study " << study.name() << " due to locale."; | |
264 return false; | |
265 } | |
266 | |
267 if (!CheckStudyPlatform(study.filter(), GetCurrentPlatform())) { | |
268 DVLOG(1) << "Filtered out study " << study.name() << " due to platform."; | |
269 return false; | |
270 } | |
271 | |
272 if (!CheckStudyVersion(study.filter(), version)) { | |
273 DVLOG(1) << "Filtered out study " << study.name() << " due to version."; | |
274 return false; | |
275 } | |
276 | |
277 if (!CheckStudyStartDate(study.filter(), reference_date)) { | |
278 DVLOG(1) << "Filtered out study " << study.name() << | |
279 " due to start date."; | |
280 return false; | |
281 } | |
282 } | |
283 | |
284 DVLOG(1) << "Kept study " << study.name() << "."; | |
285 return true; | |
286 } | |
287 | |
288 bool VariationsSeedProcessor::ValidateStudyAndComputeTotalProbability( | |
289 const Study& study, | |
290 base::FieldTrial::Probability* total_probability) { | |
291 // At the moment, a missing default_experiment_name makes the study invalid. | |
292 if (study.default_experiment_name().empty()) { | |
293 DVLOG(1) << study.name() << " has no default experiment defined."; | |
294 return false; | |
295 } | |
296 if (study.filter().has_min_version() && | |
297 !Version::IsValidWildcardString(study.filter().min_version())) { | |
298 DVLOG(1) << study.name() << " has invalid min version: " | |
299 << study.filter().min_version(); | |
300 return false; | |
301 } | |
302 if (study.filter().has_max_version() && | |
303 !Version::IsValidWildcardString(study.filter().max_version())) { | |
304 DVLOG(1) << study.name() << " has invalid max version: " | |
305 << study.filter().max_version(); | |
306 return false; | |
307 } | |
308 | |
309 const std::string& default_group_name = study.default_experiment_name(); | |
310 base::FieldTrial::Probability divisor = 0; | |
311 | |
312 bool found_default_group = false; | |
313 std::set<std::string> experiment_names; | |
314 for (int i = 0; i < study.experiment_size(); ++i) { | |
315 if (study.experiment(i).name().empty()) { | |
316 DVLOG(1) << study.name() << " is missing experiment " << i << " name"; | |
317 return false; | |
318 } | |
319 if (!experiment_names.insert(study.experiment(i).name()).second) { | |
320 DVLOG(1) << study.name() << " has a repeated experiment name " | |
321 << study.experiment(i).name(); | |
322 return false; | |
323 } | |
324 | |
325 if (!study.experiment(i).has_forcing_flag()) | |
326 divisor += study.experiment(i).probability_weight(); | |
327 if (study.experiment(i).name() == default_group_name) | |
328 found_default_group = true; | |
329 } | |
330 | |
331 if (!found_default_group) { | |
332 DVLOG(1) << study.name() << " is missing default experiment in its " | |
333 << "experiment list"; | |
334 // The default group was not found in the list of groups. This study is not | |
335 // valid. | |
336 return false; | |
337 } | |
338 | |
339 *total_probability = divisor; | |
340 return true; | |
341 } | |
342 | |
343 } // namespace chrome_variations | |
OLD | NEW |