| 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 |