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