diff options
Diffstat (limited to 'components/variations')
-rw-r--r-- | components/variations/variations_seed_simulator.cc | 187 | ||||
-rw-r--r-- | components/variations/variations_seed_simulator.h | 57 | ||||
-rw-r--r-- | components/variations/variations_seed_simulator_unittest.cc | 287 |
3 files changed, 531 insertions, 0 deletions
diff --git a/components/variations/variations_seed_simulator.cc b/components/variations/variations_seed_simulator.cc new file mode 100644 index 0000000..c0ecf43 --- /dev/null +++ b/components/variations/variations_seed_simulator.cc @@ -0,0 +1,187 @@ +// 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 <map> + +#include "base/metrics/field_trial.h" +#include "components/variations/processed_study.h" +#include "components/variations/proto/study.pb.h" +#include "components/variations/variations_associated_data.h" + +namespace chrome_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::VariationsSeedSimulator( + const base::FieldTrial::EntropyProvider& entropy_provider) + : entropy_provider_(entropy_provider) { +} + +VariationsSeedSimulator::~VariationsSeedSimulator() { +} + +int VariationsSeedSimulator::ComputeDifferences( + const std::vector<ProcessedStudy>& processed_studies) { + std::map<std::string, std::string> current_state; + GetCurrentTrialState(¤t_state); + int group_change_count = 0; + + 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; + if (study.consistency() == Study_Consistency_PERMANENT) { + if (PermanentStudyGroupChanged(processed_studies[i], selected_group)) + ++group_change_count; + } else if (study.consistency() == Study_Consistency_SESSION) { + if (SessionStudyGroupChanged(processed_studies[i], selected_group)) + ++group_change_count; + } + } + + // 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 group_change_count; +} + +bool 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 std::string simulated_group = SimulateGroupAssignment(entropy_provider_, + processed_study); + // TODO(asvitkine): Sometimes group names are changed without changing any + // behavior (e.g. if the behavior is controlled entirely via params). Support + // a mechanism to bypass this check. + if (simulated_group != selected_group) + return true; + + const Study_Experiment* experiment = FindExperiment(study, selected_group); + DCHECK(experiment); + return !VariationParamsAreEqual(study, *experiment); +} + +bool VariationsSeedSimulator::SessionStudyGroupChanged( + const ProcessedStudy& processed_study, + const std::string& selected_group) { + const Study& study = *processed_study.study(); + DCHECK_EQ(Study_Consistency_SESSION, study.consistency()); + + 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. + return true; + } + + const Study_Experiment* experiment = FindExperiment(study, selected_group); + if (!experiment) + return true; + if (experiment->probability_weight() == 0 && + !experiment->has_forcing_flag()) { + return true; + } + + // Current group exists in the study - check whether its params changed. + return !VariationParamsAreEqual(study, *experiment); +} + +} // namespace chrome_variations diff --git a/components/variations/variations_seed_simulator.h b/components/variations/variations_seed_simulator.h new file mode 100644 index 0000000..3ecec98 --- /dev/null +++ b/components/variations/variations_seed_simulator.h @@ -0,0 +1,57 @@ +// 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. + +#ifndef COMPONENTS_VARIATIONS_VARIATIONS_SEED_SIMULATOR_H_ +#define COMPONENTS_VARIATIONS_VARIATIONS_SEED_SIMULATOR_H_ + +#include <string> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/metrics/field_trial.h" + +namespace chrome_variations { + +class ProcessedStudy; + +// VariationsSeedSimulator simulates the result of creating a set of studies +// and detecting which studies would result in group changes. +class VariationsSeedSimulator { + public: + // Creates the simulator with the given entropy |provider|. + explicit VariationsSeedSimulator( + const base::FieldTrial::EntropyProvider& provider); + virtual ~VariationsSeedSimulator(); + + // Computes differences between the current process' field trial state and + // the result of evaluating the |processed_studies| list. It is expected that + // |processed_studies| have already been filtered and only contain studies + // that apply to the configuration being simulated. Returns a lower bound on + // the number of studies that are expected to change groups (lower bound due + // to session randomized studies). + int ComputeDifferences( + const std::vector<ProcessedStudy>& processed_studies); + + private: + // For the given |processed_study| with PERMANENT consistency, simulates group + // assignment and returns true if the result differs from that study's group + // in the current process. + bool PermanentStudyGroupChanged(const ProcessedStudy& processed_study, + const std::string& selected_group); + + // For the given |processed_study| with SESSION consistency, determines if + // there are enough changes in the study config that restarting will result + // in a guaranteed different group assignment (or different params). + bool SessionStudyGroupChanged(const ProcessedStudy& filtered_study, + const std::string& selected_group); + + const base::FieldTrial::EntropyProvider& entropy_provider_; + + DISALLOW_COPY_AND_ASSIGN(VariationsSeedSimulator); +}; + +} // namespace chrome_variations + +#endif // COMPONENTS_VARIATIONS_VARIATIONS_SEED_SIMULATOR_H_ diff --git a/components/variations/variations_seed_simulator_unittest.cc b/components/variations/variations_seed_simulator_unittest.cc new file mode 100644 index 0000000..b6537ea --- /dev/null +++ b/components/variations/variations_seed_simulator_unittest.cc @@ -0,0 +1,287 @@ +// 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 <map> + +#include "components/variations/processed_study.h" +#include "components/variations/proto/study.pb.h" +#include "components/variations/variations_associated_data.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace chrome_variations { + +namespace { + +// An implementation of EntropyProvider that always returns a specific entropy +// value, regardless of field trial. +class TestEntropyProvider : public base::FieldTrial::EntropyProvider { + public: + explicit TestEntropyProvider(double entropy_value) + : entropy_value_(entropy_value) {} + virtual ~TestEntropyProvider() {} + + // base::FieldTrial::EntropyProvider implementation: + virtual double GetEntropyForTrial(const std::string& trial_name, + uint32 randomization_seed) const OVERRIDE { + return entropy_value_; + } + + private: + const double entropy_value_; + + DISALLOW_COPY_AND_ASSIGN(TestEntropyProvider); +}; + +// Creates and activates a single-group field trial with name |trial_name| and +// group |group_name| and variations |params| (if not NULL). +void CreateTrial(const std::string& trial_name, + const std::string& group_name, + const std::map<std::string, std::string>* params) { + base::FieldTrialList::CreateFieldTrial(trial_name, group_name); + if (params != NULL) + AssociateVariationParams(trial_name, group_name, *params); + base::FieldTrialList::FindFullName(trial_name); +} + +// Creates a study with the given |study_name| and |consistency|. +Study CreateStudy(const std::string& study_name, + Study_Consistency consistency) { + Study study; + study.set_name(study_name); + study.set_consistency(consistency); + return study; +} + +// Adds an experiment to |study| with the specified |experiment_name| and +// |probability| values and sets it as the study's default experiment. +Study_Experiment* AddExperiment(const std::string& experiment_name, + int probability, + Study* study) { + Study_Experiment* experiment = study->add_experiment(); + experiment->set_name(experiment_name); + experiment->set_probability_weight(probability); + study->set_default_experiment_name(experiment_name); + return experiment; +} + +// Add an experiment param with |param_name| and |param_value| to |experiment|. +Study_Experiment_Param* AddExperimentParam(const std::string& param_name, + const std::string& param_value, + Study_Experiment* experiment) { + Study_Experiment_Param* param = experiment->add_param(); + param->set_name(param_name); + param->set_value(param_value); + return param; +} + +// Uses a VariationsSeedSimulator to simulate the differences between |studies| +// and the current field trial state. +int SimulateDifferences(const std::vector<ProcessedStudy>& studies) { + TestEntropyProvider provider(0.5); + VariationsSeedSimulator seed_simulator(provider); + return seed_simulator.ComputeDifferences(studies); +} + +class VariationsSeedSimulatorTest : public ::testing::Test { + public: + VariationsSeedSimulatorTest() : field_trial_list_(NULL) { + } + + virtual ~VariationsSeedSimulatorTest() { + // Ensure that the maps are cleared between tests, since they are stored as + // process singletons. + testing::ClearAllVariationIDs(); + testing::ClearAllVariationParams(); + } + + private: + base::FieldTrialList field_trial_list_; + + DISALLOW_COPY_AND_ASSIGN(VariationsSeedSimulatorTest); +}; + +} // namespace + +TEST_F(VariationsSeedSimulatorTest, PermanentNoChanges) { + CreateTrial("A", "B", NULL); + + std::vector<ProcessedStudy> processed_studies; + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + AddExperiment("B", 100, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + EXPECT_EQ(0, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, PermanentGroupChange) { + CreateTrial("A", "B", NULL); + + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + AddExperiment("C", 100, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, PermanentExpired) { + CreateTrial("A", "B", NULL); + + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + AddExperiment("B", 1, &study); + AddExperiment("C", 0, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, true, &studies)); + EXPECT_TRUE(studies[0].is_expired()); + + // There should be a difference because the study is expired, which should + // result in the default group "D" being chosen. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, SessionRandomized) { + CreateTrial("A", "B", NULL); + + Study study = CreateStudy("A", Study_Consistency_SESSION); + AddExperiment("B", 1, &study); + AddExperiment("C", 1, &study); + AddExperiment("D", 1, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + // There should be no differences, since a session randomized study can result + // in any of the groups being chosen on startup. + EXPECT_EQ(0, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, SessionRandomizedGroupRemoved) { + CreateTrial("A", "B", NULL); + + Study study = CreateStudy("A", Study_Consistency_SESSION); + AddExperiment("C", 1, &study); + AddExperiment("D", 1, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + // There should be a difference since there is no group "B" in the new config. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, SessionRandomizedGroupProbabilityZero) { + CreateTrial("A", "B", NULL); + + Study study = CreateStudy("A", Study_Consistency_SESSION); + AddExperiment("B", 0, &study); + AddExperiment("C", 1, &study); + AddExperiment("D", 1, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + // There should be a difference since there is group "B" has probability 0. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, SessionRandomizedExpired) { + CreateTrial("A", "B", NULL); + + Study study = CreateStudy("A", Study_Consistency_SESSION); + AddExperiment("B", 1, &study); + AddExperiment("C", 1, &study); + AddExperiment("D", 1, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, true, &studies)); + EXPECT_TRUE(studies[0].is_expired()); + + // There should be a difference because the study is expired, which should + // result in the default group "D" being chosen. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, ParamsUnchanged) { + std::map<std::string, std::string> params; + params["p1"] = "x"; + params["p2"] = "y"; + params["p3"] = "z"; + CreateTrial("A", "B", ¶ms); + + std::vector<ProcessedStudy> processed_studies; + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + Study_Experiment* experiment = AddExperiment("B", 100, &study); + AddExperimentParam("p2", "y", experiment); + AddExperimentParam("p1", "x", experiment); + AddExperimentParam("p3", "z", experiment); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + EXPECT_EQ(0, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, ParamsChanged) { + std::map<std::string, std::string> params; + params["p1"] = "x"; + params["p2"] = "y"; + params["p3"] = "z"; + CreateTrial("A", "B", ¶ms); + + std::vector<ProcessedStudy> processed_studies; + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + Study_Experiment* experiment = AddExperiment("B", 100, &study); + AddExperimentParam("p2", "test", experiment); + AddExperimentParam("p1", "x", experiment); + AddExperimentParam("p3", "z", experiment); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + // The param lists differ. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, ParamsRemoved) { + std::map<std::string, std::string> params; + params["p1"] = "x"; + params["p2"] = "y"; + params["p3"] = "z"; + CreateTrial("A", "B", ¶ms); + + std::vector<ProcessedStudy> processed_studies; + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + AddExperiment("B", 100, &study); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + // The current group has params, but the new config doesn't have any. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +TEST_F(VariationsSeedSimulatorTest, ParamsAdded) { + CreateTrial("A", "B", NULL); + + std::vector<ProcessedStudy> processed_studies; + Study study = CreateStudy("A", Study_Consistency_PERMANENT); + Study_Experiment* experiment = AddExperiment("B", 100, &study); + AddExperimentParam("p2", "y", experiment); + AddExperimentParam("p1", "x", experiment); + AddExperimentParam("p3", "z", experiment); + + std::vector<ProcessedStudy> studies; + EXPECT_TRUE(ProcessedStudy::ValidateAndAppendStudy(&study, false, &studies)); + + // The current group has no params, but the config has added some. + EXPECT_EQ(1, SimulateDifferences(studies)); +} + +} // namespace chrome_variations |