Home | History | Annotate | Download | only in variations
      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