|  | // Copyright 2014 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "components/variations/variations_seed_simulator.h" | 
|  |  | 
|  | #include <stddef.h> | 
|  |  | 
|  | #include <map> | 
|  |  | 
|  | #include "base/metrics/field_trial.h" | 
|  | #include "components/variations/processed_study.h" | 
|  | #include "components/variations/proto/study.pb.h" | 
|  | #include "components/variations/study_filtering.h" | 
|  | #include "components/variations/variations_associated_data.h" | 
|  | #include "components/variations/variations_seed_processor.h" | 
|  |  | 
|  | namespace variations { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // Fills in |current_state| with the current process' active field trials, as a | 
|  | // map of trial names to group names. | 
|  | void GetCurrentTrialState(std::map<std::string, std::string>* current_state) { | 
|  | base::FieldTrial::ActiveGroups trial_groups; | 
|  | base::FieldTrialList::GetActiveFieldTrialGroups(&trial_groups); | 
|  | for (size_t i = 0; i < trial_groups.size(); ++i) | 
|  | (*current_state)[trial_groups[i].trial_name] = trial_groups[i].group_name; | 
|  | } | 
|  |  | 
|  | // Simulate group assignment for the specified study with PERMANENT consistency. | 
|  | // Returns the experiment group that will be selected. Mirrors logic in | 
|  | // VariationsSeedProcessor::CreateTrialFromStudy(). | 
|  | std::string SimulateGroupAssignment( | 
|  | const base::FieldTrial::EntropyProvider& entropy_provider, | 
|  | const ProcessedStudy& processed_study) { | 
|  | const Study& study = *processed_study.study(); | 
|  | DCHECK_EQ(Study_Consistency_PERMANENT, study.consistency()); | 
|  |  | 
|  | const double entropy_value = | 
|  | entropy_provider.GetEntropyForTrial(study.name(), | 
|  | study.randomization_seed()); | 
|  | scoped_refptr<base::FieldTrial> trial( | 
|  | base::FieldTrial::CreateSimulatedFieldTrial( | 
|  | study.name(), processed_study.total_probability(), | 
|  | study.default_experiment_name(), entropy_value)); | 
|  |  | 
|  | for (int i = 0; i < study.experiment_size(); ++i) { | 
|  | const Study_Experiment& experiment = study.experiment(i); | 
|  | // TODO(asvitkine): This needs to properly handle the case where a group was | 
|  | // forced via forcing_flag in the current state, so that it is not treated | 
|  | // as changed. | 
|  | if (!experiment.has_forcing_flag() && | 
|  | experiment.name() != study.default_experiment_name()) { | 
|  | trial->AppendGroup(experiment.name(), experiment.probability_weight()); | 
|  | } | 
|  | } | 
|  | if (processed_study.is_expired()) | 
|  | trial->Disable(); | 
|  | return trial->group_name(); | 
|  | } | 
|  |  | 
|  | // Finds an experiment in |study| with name |experiment_name| and returns it, | 
|  | // or NULL if it does not exist. | 
|  | const Study_Experiment* FindExperiment(const Study& study, | 
|  | const std::string& experiment_name) { | 
|  | for (int i = 0; i < study.experiment_size(); ++i) { | 
|  | if (study.experiment(i).name() == experiment_name) | 
|  | return &study.experiment(i); | 
|  | } | 
|  | return NULL; | 
|  | } | 
|  |  | 
|  | // Checks whether experiment params set for |experiment| on |study| are exactly | 
|  | // equal to the params registered for the corresponding field trial in the | 
|  | // current process. | 
|  | bool VariationParamsAreEqual(const Study& study, | 
|  | const Study_Experiment& experiment) { | 
|  | std::map<std::string, std::string> params; | 
|  | GetVariationParams(study.name(), ¶ms); | 
|  |  | 
|  | if (static_cast<int>(params.size()) != experiment.param_size()) | 
|  | return false; | 
|  |  | 
|  | for (int i = 0; i < experiment.param_size(); ++i) { | 
|  | std::map<std::string, std::string>::const_iterator it = | 
|  | params.find(experiment.param(i).name()); | 
|  | if (it == params.end() || it->second != experiment.param(i).value()) | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | VariationsSeedSimulator::Result::Result() | 
|  | : normal_group_change_count(0), | 
|  | kill_best_effort_group_change_count(0), | 
|  | kill_critical_group_change_count(0) { | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::Result::~Result() { | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::VariationsSeedSimulator( | 
|  | const base::FieldTrial::EntropyProvider& default_entropy_provider, | 
|  | const base::FieldTrial::EntropyProvider& low_entropy_provider) | 
|  | : default_entropy_provider_(default_entropy_provider), | 
|  | low_entropy_provider_(low_entropy_provider) {} | 
|  |  | 
|  | VariationsSeedSimulator::~VariationsSeedSimulator() { | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::Result VariationsSeedSimulator::SimulateSeedStudies( | 
|  | const VariationsSeed& seed, | 
|  | const std::string& locale, | 
|  | const base::Time& reference_date, | 
|  | const base::Version& version, | 
|  | Study_Channel channel, | 
|  | Study_FormFactor form_factor, | 
|  | const std::string& hardware_class, | 
|  | const std::string& session_consistency_country, | 
|  | const std::string& permanent_consistency_country) { | 
|  | std::vector<ProcessedStudy> filtered_studies; | 
|  | FilterAndValidateStudies(seed, locale, reference_date, version, channel, | 
|  | form_factor, hardware_class, | 
|  | session_consistency_country, | 
|  | permanent_consistency_country, &filtered_studies); | 
|  |  | 
|  | return ComputeDifferences(filtered_studies); | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::Result VariationsSeedSimulator::ComputeDifferences( | 
|  | const std::vector<ProcessedStudy>& processed_studies) { | 
|  | std::map<std::string, std::string> current_state; | 
|  | GetCurrentTrialState(¤t_state); | 
|  |  | 
|  | Result result; | 
|  | for (size_t i = 0; i < processed_studies.size(); ++i) { | 
|  | const Study& study = *processed_studies[i].study(); | 
|  | std::map<std::string, std::string>::const_iterator it = | 
|  | current_state.find(study.name()); | 
|  |  | 
|  | // Skip studies that aren't activated in the current state. | 
|  | // TODO(asvitkine): This should be handled more intelligently. There are | 
|  | // several cases that fall into this category: | 
|  | //   1) There's an existing field trial with this name but it is not active. | 
|  | //   2) There's an existing expired field trial with this name, which is | 
|  | //      also not considered as active. | 
|  | //   3) This is a new study config that previously didn't exist. | 
|  | // The above cases should be differentiated and handled explicitly. | 
|  | if (it == current_state.end()) | 
|  | continue; | 
|  |  | 
|  | // Study exists in the current state, check whether its group will change. | 
|  | // Note: The logic below does the right thing if study consistency changes, | 
|  | // as it doesn't rely on the previous study consistency. | 
|  | const std::string& selected_group = it->second; | 
|  | ChangeType change_type = NO_CHANGE; | 
|  | if (study.consistency() == Study_Consistency_PERMANENT) { | 
|  | change_type = PermanentStudyGroupChanged(processed_studies[i], | 
|  | selected_group); | 
|  | } else if (study.consistency() == Study_Consistency_SESSION) { | 
|  | change_type = SessionStudyGroupChanged(processed_studies[i], | 
|  | selected_group); | 
|  | } | 
|  |  | 
|  | switch (change_type) { | 
|  | case NO_CHANGE: | 
|  | break; | 
|  | case CHANGED: | 
|  | ++result.normal_group_change_count; | 
|  | break; | 
|  | case CHANGED_KILL_BEST_EFFORT: | 
|  | ++result.kill_best_effort_group_change_count; | 
|  | break; | 
|  | case CHANGED_KILL_CRITICAL: | 
|  | ++result.kill_critical_group_change_count; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | // TODO(asvitkine): Handle removed studies (i.e. studies that existed in the | 
|  | // old seed, but were removed). This will require tracking the set of studies | 
|  | // that were created from the original seed. | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::ChangeType | 
|  | VariationsSeedSimulator::ConvertExperimentTypeToChangeType( | 
|  | Study_Experiment_Type type) { | 
|  | switch (type) { | 
|  | case Study_Experiment_Type_NORMAL: | 
|  | return CHANGED; | 
|  | case Study_Experiment_Type_IGNORE_CHANGE: | 
|  | return NO_CHANGE; | 
|  | case Study_Experiment_Type_KILL_BEST_EFFORT: | 
|  | return CHANGED_KILL_BEST_EFFORT; | 
|  | case Study_Experiment_Type_KILL_CRITICAL: | 
|  | return CHANGED_KILL_CRITICAL; | 
|  | } | 
|  | return CHANGED; | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::ChangeType | 
|  | VariationsSeedSimulator::PermanentStudyGroupChanged( | 
|  | const ProcessedStudy& processed_study, | 
|  | const std::string& selected_group) { | 
|  | const Study& study = *processed_study.study(); | 
|  | DCHECK_EQ(Study_Consistency_PERMANENT, study.consistency()); | 
|  |  | 
|  | const base::FieldTrial::EntropyProvider& entropy_provider = | 
|  | VariationsSeedProcessor::ShouldStudyUseLowEntropy(study) | 
|  | ? low_entropy_provider_ | 
|  | : default_entropy_provider_; | 
|  |  | 
|  | const std::string simulated_group = | 
|  | SimulateGroupAssignment(entropy_provider, processed_study); | 
|  | const Study_Experiment* experiment = FindExperiment(study, selected_group); | 
|  | if (simulated_group != selected_group) { | 
|  | if (experiment) | 
|  | return ConvertExperimentTypeToChangeType(experiment->type()); | 
|  | return CHANGED; | 
|  | } | 
|  |  | 
|  | // Current group exists in the study - check whether its params changed. | 
|  | DCHECK(experiment); | 
|  | if (!VariationParamsAreEqual(study, *experiment)) | 
|  | return ConvertExperimentTypeToChangeType(experiment->type()); | 
|  | return NO_CHANGE; | 
|  | } | 
|  |  | 
|  | VariationsSeedSimulator::ChangeType | 
|  | VariationsSeedSimulator::SessionStudyGroupChanged( | 
|  | const ProcessedStudy& processed_study, | 
|  | const std::string& selected_group) { | 
|  | const Study& study = *processed_study.study(); | 
|  | DCHECK_EQ(Study_Consistency_SESSION, study.consistency()); | 
|  |  | 
|  | const Study_Experiment* experiment = FindExperiment(study, selected_group); | 
|  | if (processed_study.is_expired() && | 
|  | selected_group != study.default_experiment_name()) { | 
|  | // An expired study will result in the default group being selected - mark | 
|  | // it as changed if the current group differs from the default. | 
|  | if (experiment) | 
|  | return ConvertExperimentTypeToChangeType(experiment->type()); | 
|  | return CHANGED; | 
|  | } | 
|  |  | 
|  | if (!experiment) | 
|  | return CHANGED; | 
|  | if (experiment->probability_weight() == 0 && | 
|  | !experiment->has_forcing_flag()) { | 
|  | return ConvertExperimentTypeToChangeType(experiment->type()); | 
|  | } | 
|  |  | 
|  | // Current group exists in the study - check whether its params changed. | 
|  | if (!VariationParamsAreEqual(study, *experiment)) | 
|  | return ConvertExperimentTypeToChangeType(experiment->type()); | 
|  | return NO_CHANGE; | 
|  | } | 
|  |  | 
|  | }  // namespace variations |