1// Copyright 2014 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_simulator.h"
6
7#include <map>
8
9#include "base/metrics/field_trial.h"
10#include "components/variations/processed_study.h"
11#include "components/variations/proto/study.pb.h"
12#include "components/variations/study_filtering.h"
13#include "components/variations/variations_associated_data.h"
14
15namespace chrome_variations {
16
17namespace {
18
19// Fills in |current_state| with the current process' active field trials, as a
20// map of trial names to group names.
21void GetCurrentTrialState(std::map<std::string, std::string>* current_state) {
22  base::FieldTrial::ActiveGroups trial_groups;
23  base::FieldTrialList::GetActiveFieldTrialGroups(&trial_groups);
24  for (size_t i = 0; i < trial_groups.size(); ++i)
25    (*current_state)[trial_groups[i].trial_name] = trial_groups[i].group_name;
26}
27
28// Simulate group assignment for the specified study with PERMANENT consistency.
29// Returns the experiment group that will be selected. Mirrors logic in
30// VariationsSeedProcessor::CreateTrialFromStudy().
31std::string SimulateGroupAssignment(
32    const base::FieldTrial::EntropyProvider& entropy_provider,
33    const ProcessedStudy& processed_study) {
34  const Study& study = *processed_study.study();
35  DCHECK_EQ(Study_Consistency_PERMANENT, study.consistency());
36
37  const double entropy_value =
38      entropy_provider.GetEntropyForTrial(study.name(),
39                                          study.randomization_seed());
40  scoped_refptr<base::FieldTrial> trial(
41      base::FieldTrial::CreateSimulatedFieldTrial(
42          study.name(), processed_study.total_probability(),
43          study.default_experiment_name(), entropy_value));
44
45  for (int i = 0; i < study.experiment_size(); ++i) {
46    const Study_Experiment& experiment = study.experiment(i);
47    // TODO(asvitkine): This needs to properly handle the case where a group was
48    // forced via forcing_flag in the current state, so that it is not treated
49    // as changed.
50    if (!experiment.has_forcing_flag() &&
51        experiment.name() != study.default_experiment_name()) {
52      trial->AppendGroup(experiment.name(), experiment.probability_weight());
53    }
54  }
55  if (processed_study.is_expired())
56    trial->Disable();
57  return trial->group_name();
58}
59
60// Finds an experiment in |study| with name |experiment_name| and returns it,
61// or NULL if it does not exist.
62const Study_Experiment* FindExperiment(const Study& study,
63                                       const std::string& experiment_name) {
64  for (int i = 0; i < study.experiment_size(); ++i) {
65    if (study.experiment(i).name() == experiment_name)
66      return &study.experiment(i);
67  }
68  return NULL;
69}
70
71// Checks whether experiment params set for |experiment| on |study| are exactly
72// equal to the params registered for the corresponding field trial in the
73// current process.
74bool VariationParamsAreEqual(const Study& study,
75                             const Study_Experiment& experiment) {
76  std::map<std::string, std::string> params;
77  GetVariationParams(study.name(), &params);
78
79  if (static_cast<int>(params.size()) != experiment.param_size())
80    return false;
81
82  for (int i = 0; i < experiment.param_size(); ++i) {
83    std::map<std::string, std::string>::const_iterator it =
84        params.find(experiment.param(i).name());
85    if (it == params.end() || it->second != experiment.param(i).value())
86      return false;
87  }
88
89  return true;
90}
91
92}  // namespace
93
94VariationsSeedSimulator::Result::Result()
95    : normal_group_change_count(0),
96      kill_best_effort_group_change_count(0),
97      kill_critical_group_change_count(0) {
98}
99
100VariationsSeedSimulator::Result::~Result() {
101}
102
103VariationsSeedSimulator::VariationsSeedSimulator(
104    const base::FieldTrial::EntropyProvider& entropy_provider)
105    : entropy_provider_(entropy_provider) {
106}
107
108VariationsSeedSimulator::~VariationsSeedSimulator() {
109}
110
111VariationsSeedSimulator::Result VariationsSeedSimulator::SimulateSeedStudies(
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    const std::string& hardware_class) {
119  std::vector<ProcessedStudy> filtered_studies;
120  FilterAndValidateStudies(seed, locale, reference_date, version, channel,
121                           form_factor, hardware_class, &filtered_studies);
122
123  return ComputeDifferences(filtered_studies);
124}
125
126VariationsSeedSimulator::Result VariationsSeedSimulator::ComputeDifferences(
127    const std::vector<ProcessedStudy>& processed_studies) {
128  std::map<std::string, std::string> current_state;
129  GetCurrentTrialState(&current_state);
130
131  Result result;
132  for (size_t i = 0; i < processed_studies.size(); ++i) {
133    const Study& study = *processed_studies[i].study();
134    std::map<std::string, std::string>::const_iterator it =
135        current_state.find(study.name());
136
137    // Skip studies that aren't activated in the current state.
138    // TODO(asvitkine): This should be handled more intelligently. There are
139    // several cases that fall into this category:
140    //   1) There's an existing field trial with this name but it is not active.
141    //   2) There's an existing expired field trial with this name, which is
142    //      also not considered as active.
143    //   3) This is a new study config that previously didn't exist.
144    // The above cases should be differentiated and handled explicitly.
145    if (it == current_state.end())
146      continue;
147
148    // Study exists in the current state, check whether its group will change.
149    // Note: The logic below does the right thing if study consistency changes,
150    // as it doesn't rely on the previous study consistency.
151    const std::string& selected_group = it->second;
152    ChangeType change_type = NO_CHANGE;
153    if (study.consistency() == Study_Consistency_PERMANENT) {
154      change_type = PermanentStudyGroupChanged(processed_studies[i],
155                                               selected_group);
156    } else if (study.consistency() == Study_Consistency_SESSION) {
157      change_type = SessionStudyGroupChanged(processed_studies[i],
158                                             selected_group);
159    }
160
161    switch (change_type) {
162      case NO_CHANGE:
163        break;
164      case CHANGED:
165        ++result.normal_group_change_count;
166        break;
167      case CHANGED_KILL_BEST_EFFORT:
168        ++result.kill_best_effort_group_change_count;
169        break;
170      case CHANGED_KILL_CRITICAL:
171        ++result.kill_critical_group_change_count;
172        break;
173    }
174  }
175
176  // TODO(asvitkine): Handle removed studies (i.e. studies that existed in the
177  // old seed, but were removed). This will require tracking the set of studies
178  // that were created from the original seed.
179
180  return result;
181}
182
183VariationsSeedSimulator::ChangeType
184VariationsSeedSimulator::ConvertExperimentTypeToChangeType(
185    Study_Experiment_Type type) {
186  switch (type) {
187    case Study_Experiment_Type_NORMAL:
188      return CHANGED;
189    case Study_Experiment_Type_IGNORE_CHANGE:
190      return NO_CHANGE;
191    case Study_Experiment_Type_KILL_BEST_EFFORT:
192      return CHANGED_KILL_BEST_EFFORT;
193    case Study_Experiment_Type_KILL_CRITICAL:
194      return CHANGED_KILL_CRITICAL;
195  }
196  return CHANGED;
197}
198
199VariationsSeedSimulator::ChangeType
200VariationsSeedSimulator::PermanentStudyGroupChanged(
201    const ProcessedStudy& processed_study,
202    const std::string& selected_group) {
203  const Study& study = *processed_study.study();
204  DCHECK_EQ(Study_Consistency_PERMANENT, study.consistency());
205
206  const std::string simulated_group = SimulateGroupAssignment(entropy_provider_,
207                                                              processed_study);
208  const Study_Experiment* experiment = FindExperiment(study, selected_group);
209  if (simulated_group != selected_group) {
210    if (experiment)
211      return ConvertExperimentTypeToChangeType(experiment->type());
212    return CHANGED;
213  }
214
215  // Current group exists in the study - check whether its params changed.
216  DCHECK(experiment);
217  if (!VariationParamsAreEqual(study, *experiment))
218    return ConvertExperimentTypeToChangeType(experiment->type());
219  return NO_CHANGE;
220}
221
222VariationsSeedSimulator::ChangeType
223VariationsSeedSimulator::SessionStudyGroupChanged(
224    const ProcessedStudy& processed_study,
225    const std::string& selected_group) {
226  const Study& study = *processed_study.study();
227  DCHECK_EQ(Study_Consistency_SESSION, study.consistency());
228
229  const Study_Experiment* experiment = FindExperiment(study, selected_group);
230  if (processed_study.is_expired() &&
231      selected_group != study.default_experiment_name()) {
232    // An expired study will result in the default group being selected - mark
233    // it as changed if the current group differs from the default.
234    if (experiment)
235      return ConvertExperimentTypeToChangeType(experiment->type());
236    return CHANGED;
237  }
238
239  if (!experiment)
240    return CHANGED;
241  if (experiment->probability_weight() == 0 &&
242      !experiment->has_forcing_flag()) {
243    return ConvertExperimentTypeToChangeType(experiment->type());
244  }
245
246  // Current group exists in the study - check whether its params changed.
247  if (!VariationParamsAreEqual(study, *experiment))
248    return ConvertExperimentTypeToChangeType(experiment->type());
249  return NO_CHANGE;
250}
251
252}  // namespace chrome_variations
253