summaryrefslogtreecommitdiffstats
path: root/components/variations
diff options
context:
space:
mode:
Diffstat (limited to 'components/variations')
-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
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(), &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