summaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
authorasvitkine@chromium.org <asvitkine@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-01-08 18:21:00 +0000
committerasvitkine@chromium.org <asvitkine@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-01-08 18:21:00 +0000
commit92e09a18cda049db231140ea0c262e7a3bf52740 (patch)
tree761d3d82e0a8df214ad735200d0922b0b1624a8d /components
parentd72493154d4cbf5271993496f7e842ef31c357ad (diff)
downloadchromium_src-92e09a18cda049db231140ea0c262e7a3bf52740.zip
chromium_src-92e09a18cda049db231140ea0c262e7a3bf52740.tar.gz
chromium_src-92e09a18cda049db231140ea0c262e7a3bf52740.tar.bz2
Implement initial version of VariationSeedSimulator.
This will be used by the VariationsService to simulate newly received variations seed, to evaluate whether there's sufficient new changes to badge the UI to suggest a restart (similar to what happens during auto updates). This is the initial version of this class and still has some missing pieces, left as TODOs. BUG=315807 TEST=Unit tests. TBR=joi@chromium.org Review URL: https://codereview.chromium.org/41623002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@243600 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'components')
-rw-r--r--components/components_tests.gyp1
-rw-r--r--components/variations.gypi2
-rw-r--r--components/variations/variations_seed_simulator.cc187
-rw-r--r--components/variations/variations_seed_simulator.h57
-rw-r--r--components/variations/variations_seed_simulator_unittest.cc287
5 files changed, 534 insertions, 0 deletions
diff --git a/components/components_tests.gyp b/components/components_tests.gyp
index 9d0f653..7dca5d6 100644
--- a/components/components_tests.gyp
+++ b/components/components_tests.gyp
@@ -62,6 +62,7 @@
'variations/metrics_util_unittest.cc',
'variations/variations_associated_data_unittest.cc',
'variations/variations_seed_processor_unittest.cc',
+ 'variations/variations_seed_simulator_unittest.cc',
'visitedlink/test/visitedlink_unittest.cc',
'webdata/encryptor/encryptor_password_mac_unittest.cc',
'webdata/encryptor/encryptor_unittest.cc',
diff --git a/components/variations.gypi b/components/variations.gypi
index 4a69d65..13d8a83 100644
--- a/components/variations.gypi
+++ b/components/variations.gypi
@@ -27,6 +27,8 @@
'variations/variations_associated_data.h',
'variations/variations_seed_processor.cc',
'variations/variations_seed_processor.h',
+ 'variations/variations_seed_simulator.cc',
+ 'variations/variations_seed_simulator.h',
],
'variables': {
'proto_in_dir': 'variations/proto',
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(), &params);
+
+ 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(&current_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", &params);
+
+ 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", &params);
+
+ 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", &params);
+
+ 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