blob: f711c8d155210b4fc9ba043064374aca8af0e933 [file] [log] [blame]
// 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/client_filterable_state.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(),
processed_study.GetDefaultExperimentName(), 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 nullptr;
}
// 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::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 ClientFilterableState& client_state) {
std::vector<ProcessedStudy> filtered_studies;
FilterAndValidateStudies(seed, client_state, &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(&current_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);
// Note: The current (i.e. old) group is checked for the type since that group
// is the one that should be annotated with the type when killing it.
const Study_Experiment* experiment = FindExperiment(study, selected_group);
if (simulated_group != selected_group) {
if (experiment)
return ConvertExperimentTypeToChangeType(experiment->type());
return CHANGED;
}
// If the group is unchanged, check whether its params may have changed.
if (experiment && !VariationParamsAreEqual(study, *experiment))
return ConvertExperimentTypeToChangeType(experiment->type());
// Since the group name has not changed and params are either equal or the
// experiment was not found (and thus there are none), return NO_CHANGE.
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