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/processed_study.h" 16 #include "components/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 // Associates the variations params of |experiment|, if present. 48 void RegisterExperimentParams(const Study& study, 49 const Study_Experiment& experiment) { 50 std::map<std::string, std::string> params; 51 for (int i = 0; i < experiment.param_size(); ++i) { 52 if (experiment.param(i).has_name() && experiment.param(i).has_value()) 53 params[experiment.param(i).name()] = experiment.param(i).value(); 54 } 55 if (!params.empty()) 56 AssociateVariationParams(study.name(), experiment.name(), params); 57 } 58 59 // If there are variation ids associated with |experiment|, register the 60 // variation ids. 61 void RegisterVariationIds(const Study_Experiment& experiment, 62 const std::string& trial_name) { 63 if (experiment.has_google_web_experiment_id()) { 64 const VariationID variation_id = 65 static_cast<VariationID>(experiment.google_web_experiment_id()); 66 AssociateGoogleVariationIDForce(GOOGLE_WEB_PROPERTIES, 67 trial_name, 68 experiment.name(), 69 variation_id); 70 } 71 if (experiment.has_google_update_experiment_id()) { 72 const VariationID variation_id = 73 static_cast<VariationID>(experiment.google_update_experiment_id()); 74 AssociateGoogleVariationIDForce(GOOGLE_UPDATE_SERVICE, 75 trial_name, 76 experiment.name(), 77 variation_id); 78 } 79 } 80 81 } // namespace 82 83 VariationsSeedProcessor::VariationsSeedProcessor() { 84 } 85 86 VariationsSeedProcessor::~VariationsSeedProcessor() { 87 } 88 89 bool VariationsSeedProcessor::AllowVariationIdWithForcingFlag( 90 const Study& study) { 91 if (!study.has_filter()) 92 return false; 93 const Study_Filter& filter = study.filter(); 94 if (filter.platform_size() == 0 || filter.channel_size() == 0) 95 return false; 96 for (int i = 0; i < filter.platform_size(); ++i) { 97 if (filter.platform(i) != Study_Platform_PLATFORM_ANDROID && 98 filter.platform(i) != Study_Platform_PLATFORM_IOS) { 99 return false; 100 } 101 } 102 for (int i = 0; i < filter.channel_size(); ++i) { 103 if (filter.channel(i) != Study_Channel_CANARY && 104 filter.channel(i) != Study_Channel_DEV) { 105 return false; 106 } 107 } 108 return true; 109 } 110 111 void VariationsSeedProcessor::CreateTrialsFromSeed( 112 const VariationsSeed& seed, 113 const std::string& locale, 114 const base::Time& reference_date, 115 const base::Version& version, 116 Study_Channel channel, 117 Study_FormFactor form_factor) { 118 std::vector<ProcessedStudy> filtered_studies; 119 FilterAndValidateStudies(seed, locale, reference_date, version, channel, 120 form_factor, &filtered_studies); 121 122 for (size_t i = 0; i < filtered_studies.size(); ++i) 123 CreateTrialFromStudy(filtered_studies[i]); 124 } 125 126 void VariationsSeedProcessor::FilterAndValidateStudies( 127 const VariationsSeed& seed, 128 const std::string& locale, 129 const base::Time& reference_date, 130 const base::Version& version, 131 Study_Channel channel, 132 Study_FormFactor form_factor, 133 std::vector<ProcessedStudy>* filtered_studies) { 134 DCHECK(version.IsValid()); 135 136 // Add expired studies (in a disabled state) only after all the non-expired 137 // studies have been added (and do not add an expired study if a corresponding 138 // non-expired study got added). This way, if there's both an expired and a 139 // non-expired study that applies, the non-expired study takes priority. 140 std::set<std::string> created_studies; 141 std::vector<const Study*> expired_studies; 142 143 for (int i = 0; i < seed.study_size(); ++i) { 144 const Study& study = seed.study(i); 145 if (!ShouldAddStudy(study, locale, reference_date, 146 version, channel, form_factor)) 147 continue; 148 149 if (IsStudyExpired(study, reference_date)) { 150 expired_studies.push_back(&study); 151 } else if (!ContainsKey(created_studies, study.name())) { 152 ProcessedStudy::ValidateAndAppendStudy(&study, false, filtered_studies); 153 created_studies.insert(study.name()); 154 } 155 } 156 157 for (size_t i = 0; i < expired_studies.size(); ++i) { 158 if (!ContainsKey(created_studies, expired_studies[i]->name())) { 159 ProcessedStudy::ValidateAndAppendStudy(expired_studies[i], true, 160 filtered_studies); 161 } 162 } 163 } 164 165 bool VariationsSeedProcessor::CheckStudyChannel(const Study_Filter& filter, 166 Study_Channel channel) { 167 // An empty channel list matches all channels. 168 if (filter.channel_size() == 0) 169 return true; 170 171 for (int i = 0; i < filter.channel_size(); ++i) { 172 if (filter.channel(i) == channel) 173 return true; 174 } 175 return false; 176 } 177 178 bool VariationsSeedProcessor::CheckStudyFormFactor( 179 const Study_Filter& filter, 180 Study_FormFactor form_factor) { 181 // An empty form factor list matches all form factors. 182 if (filter.form_factor_size() == 0) 183 return true; 184 185 for (int i = 0; i < filter.form_factor_size(); ++i) { 186 if (filter.form_factor(i) == form_factor) 187 return true; 188 } 189 return false; 190 } 191 192 bool VariationsSeedProcessor::CheckStudyLocale( 193 const Study_Filter& filter, 194 const std::string& locale) { 195 // An empty locale list matches all locales. 196 if (filter.locale_size() == 0) 197 return true; 198 199 for (int i = 0; i < filter.locale_size(); ++i) { 200 if (filter.locale(i) == locale) 201 return true; 202 } 203 return false; 204 } 205 206 bool VariationsSeedProcessor::CheckStudyPlatform( 207 const Study_Filter& filter, 208 Study_Platform platform) { 209 // An empty platform list matches all platforms. 210 if (filter.platform_size() == 0) 211 return true; 212 213 for (int i = 0; i < filter.platform_size(); ++i) { 214 if (filter.platform(i) == platform) 215 return true; 216 } 217 return false; 218 } 219 220 bool VariationsSeedProcessor::CheckStudyStartDate( 221 const Study_Filter& filter, 222 const base::Time& date_time) { 223 if (filter.has_start_date()) { 224 const base::Time start_date = 225 ConvertStudyDateToBaseTime(filter.start_date()); 226 return date_time >= start_date; 227 } 228 229 return true; 230 } 231 232 bool VariationsSeedProcessor::CheckStudyVersion( 233 const Study_Filter& filter, 234 const base::Version& version) { 235 if (filter.has_min_version()) { 236 if (version.CompareToWildcardString(filter.min_version()) < 0) 237 return false; 238 } 239 240 if (filter.has_max_version()) { 241 if (version.CompareToWildcardString(filter.max_version()) > 0) 242 return false; 243 } 244 245 return true; 246 } 247 248 void VariationsSeedProcessor::CreateTrialFromStudy( 249 const ProcessedStudy& processed_study) { 250 const Study& study = *processed_study.study(); 251 252 // Check if any experiments need to be forced due to a command line 253 // flag. Force the first experiment with an existing flag. 254 CommandLine* command_line = CommandLine::ForCurrentProcess(); 255 for (int i = 0; i < study.experiment_size(); ++i) { 256 const Study_Experiment& experiment = study.experiment(i); 257 if (experiment.has_forcing_flag() && 258 command_line->HasSwitch(experiment.forcing_flag())) { 259 base::FieldTrialList::CreateFieldTrial(study.name(), experiment.name()); 260 RegisterExperimentParams(study, experiment); 261 DVLOG(1) << "Trial " << study.name() << " forced by flag: " 262 << experiment.forcing_flag(); 263 if (AllowVariationIdWithForcingFlag(study)) 264 RegisterVariationIds(experiment, study.name()); 265 return; 266 } 267 } 268 269 uint32 randomization_seed = 0; 270 base::FieldTrial::RandomizationType randomization_type = 271 base::FieldTrial::SESSION_RANDOMIZED; 272 if (study.has_consistency() && 273 study.consistency() == Study_Consistency_PERMANENT) { 274 randomization_type = base::FieldTrial::ONE_TIME_RANDOMIZED; 275 if (study.has_randomization_seed()) 276 randomization_seed = study.randomization_seed(); 277 } 278 279 // The trial is created without specifying an expiration date because the 280 // expiration check in field_trial.cc is based on the build date. Instead, 281 // the expiration check using |reference_date| is done explicitly below. 282 scoped_refptr<base::FieldTrial> trial( 283 base::FieldTrialList::FactoryGetFieldTrialWithRandomizationSeed( 284 study.name(), processed_study.total_probability(), 285 study.default_experiment_name(), 286 base::FieldTrialList::kNoExpirationYear, 1, 1, randomization_type, 287 randomization_seed, NULL)); 288 289 for (int i = 0; i < study.experiment_size(); ++i) { 290 const Study_Experiment& experiment = study.experiment(i); 291 RegisterExperimentParams(study, experiment); 292 293 // Groups with forcing flags have probability 0 and will never be selected. 294 // Therefore, there's no need to add them to the field trial. 295 if (experiment.has_forcing_flag()) 296 continue; 297 298 if (experiment.name() != study.default_experiment_name()) 299 trial->AppendGroup(experiment.name(), experiment.probability_weight()); 300 301 RegisterVariationIds(experiment, study.name()); 302 } 303 304 trial->SetForced(); 305 if (processed_study.is_expired()) 306 trial->Disable(); 307 else if (study.activation_type() == Study_ActivationType_ACTIVATION_AUTO) 308 trial->group(); 309 } 310 311 bool VariationsSeedProcessor::IsStudyExpired(const Study& study, 312 const base::Time& date_time) { 313 if (study.has_expiry_date()) { 314 const base::Time expiry_date = 315 ConvertStudyDateToBaseTime(study.expiry_date()); 316 return date_time >= expiry_date; 317 } 318 319 return false; 320 } 321 322 bool VariationsSeedProcessor::ShouldAddStudy( 323 const Study& study, 324 const std::string& locale, 325 const base::Time& reference_date, 326 const base::Version& version, 327 Study_Channel channel, 328 Study_FormFactor form_factor) { 329 if (study.has_filter()) { 330 if (!CheckStudyChannel(study.filter(), channel)) { 331 DVLOG(1) << "Filtered out study " << study.name() << " due to channel."; 332 return false; 333 } 334 335 if (!CheckStudyFormFactor(study.filter(), form_factor)) { 336 DVLOG(1) << "Filtered out study " << study.name() << 337 " due to form factor."; 338 return false; 339 } 340 341 if (!CheckStudyLocale(study.filter(), locale)) { 342 DVLOG(1) << "Filtered out study " << study.name() << " due to locale."; 343 return false; 344 } 345 346 if (!CheckStudyPlatform(study.filter(), GetCurrentPlatform())) { 347 DVLOG(1) << "Filtered out study " << study.name() << " due to platform."; 348 return false; 349 } 350 351 if (!CheckStudyVersion(study.filter(), version)) { 352 DVLOG(1) << "Filtered out study " << study.name() << " due to version."; 353 return false; 354 } 355 356 if (!CheckStudyStartDate(study.filter(), reference_date)) { 357 DVLOG(1) << "Filtered out study " << study.name() << 358 " due to start date."; 359 return false; 360 } 361 } 362 363 DVLOG(1) << "Kept study " << study.name() << "."; 364 return true; 365 } 366 367 } // namespace chrome_variations 368