blob: 880b2705f2a323bf621f9cca20c996821727484b [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/variations/field_trial_config/field_trial_util.h"
#include <stddef.h>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/system/sys_info.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/field_trial_config/fieldtrial_testing_config.h"
#include "components/variations/study_filtering.h"
#include "components/variations/variations_seed_processor.h"
#include "components/variations/variations_switches.h"
namespace variations {
namespace {
bool HasPlatform(const FieldTrialTestingExperiment& experiment,
Study::Platform platform) {
for (Study::Platform experiment_platform : experiment.platforms) {
if (experiment_platform == platform) {
return true;
}
}
return false;
}
// Returns true if the experiment config has a different value for
// is_low_end_device than the current system value does.
// If experiment has is_low_end_device missing, then it is False.
bool HasDeviceLevelMismatch(const FieldTrialTestingExperiment& experiment) {
if (!experiment.is_low_end_device.has_value()) {
return false;
}
return experiment.is_low_end_device.value() !=
base::SysInfo::IsLowEndDevice();
}
// Returns true if the experiment config has a missing form_factors or it
// contains the current system's form_factor. Otherwise, it is False.
bool HasFormFactor(const FieldTrialTestingExperiment& experiment,
Study::FormFactor current_form_factor) {
for (Study::FormFactor experiment_form_factor : experiment.form_factors) {
if (experiment_form_factor == current_form_factor) {
return true;
}
}
return experiment.form_factors.size() == 0;
}
// Returns true if the experiment config has a missing |min_os_version| or
// GetOSVersion() >= |min_os_version|.
bool HasMinOSVersion(const FieldTrialTestingExperiment& experiment) {
if (!experiment.min_os_version) {
return true;
}
return base::Version(experiment.min_os_version) <=
ClientFilterableState::GetOSVersion();
}
// Checks that if |is_benchmarking_enabled| is true that this particular
// experiment has not been disabled for benchmarking.
bool IsEnabledForBenchmarking(const FieldTrialTestingExperiment& experiment,
const bool is_benchmarking_enabled) {
return !is_benchmarking_enabled ||
!experiment.disable_benchmarking.value_or(false);
}
// Records the override ui string config. Mainly used for testing.
void ApplyUIStringOverrides(
const FieldTrialTestingExperiment& experiment,
const VariationsSeedProcessor::UIStringOverrideCallback& callback) {
for (const auto& override_ui_string : experiment.override_ui_string) {
callback.Run(override_ui_string.name_hash,
base::UTF8ToUTF16(override_ui_string.value));
}
}
// Determines whether an experiment should be skipped or not. An experiment
// should be skipped if it enables or disables a feature that is already
// overridden through the command line.
bool ShouldSkipExperiment(const FieldTrialTestingExperiment& experiment,
base::FeatureList* feature_list) {
for (const auto* enabled_feature : experiment.enable_features) {
if (feature_list->IsFeatureOverridden(enabled_feature)) {
return true;
}
}
for (const auto* disabled_feature : experiment.disable_features) {
if (feature_list->IsFeatureOverridden(disabled_feature)) {
return true;
}
}
return false;
}
void AssociateParamsFromExperiment(
const std::string& study_name,
const FieldTrialTestingExperiment& experiment,
const VariationsSeedProcessor::UIStringOverrideCallback& callback,
base::FeatureList* feature_list) {
if (ShouldSkipExperiment(experiment, feature_list)) {
return;
}
if (experiment.params.size() != 0) {
base::FieldTrialParams params;
for (const FieldTrialTestingExperimentParams& param : experiment.params) {
params[param.key] = param.value;
}
base::AssociateFieldTrialParams(study_name, experiment.name, params);
}
base::FieldTrial* trial =
base::FieldTrialList::CreateFieldTrial(study_name, experiment.name);
if (!trial) {
return;
}
for (const auto* enabled_feature : experiment.enable_features) {
feature_list->RegisterFieldTrialOverride(
enabled_feature, base::FeatureList::OVERRIDE_ENABLE_FEATURE, trial);
}
for (const auto* disabled_feature : experiment.disable_features) {
feature_list->RegisterFieldTrialOverride(
disabled_feature, base::FeatureList::OVERRIDE_DISABLE_FEATURE, trial);
}
ApplyUIStringOverrides(experiment, callback);
}
Study::Filter CreateFilter(const FieldTrialTestingExperiment& experiment) {
Study::Filter filter;
for (const auto* included_hw_class : experiment.hardware_classes) {
filter.add_hardware_class(included_hw_class);
}
for (const auto* excluded_hw_class : experiment.exclude_hardware_classes) {
filter.add_exclude_hardware_class(excluded_hw_class);
}
return filter;
}
// Choose an experiment to associate. The rules are:
// - Out of the experiments which match this platform:
// - If there is a forcing flag for any experiment, choose the first such
// experiment.
// - Otherwise, If running on low_end_device and the config specify
// a different experiment group for low end devices then pick that.
// - Otherwise, If running on non low_end_device and the config specify
// a different experiment group for non low_end_device then pick that.
// - Otherwise, select the first experiment.
// - The chosen experiment must not enable or disable a feature that is
// explicitly enabled or disabled through a switch, such as the
// |--enable-features| or |--disable-features| switches. If it does, then no
// experiment is associated.
// - If no experiments match this platform, do not associate any of them.
void ChooseExperiment(
const FieldTrialTestingStudy& study,
const VariationsSeedProcessor::UIStringOverrideCallback& callback,
Study::Platform platform,
Study::FormFactor current_form_factor,
base::FeatureList* feature_list) {
const auto& command_line = *base::CommandLine::ForCurrentProcess();
std::string hardware_class = ClientFilterableState::GetHardwareClass();
const bool is_benchmarking_enabled =
command_line.HasSwitch(switches::kEnableBenchmarking);
const FieldTrialTestingExperiment* chosen_experiment = nullptr;
for (const FieldTrialTestingExperiment& experiment : study.experiments) {
if (HasPlatform(experiment, platform)) {
Study::Filter filter = CreateFilter(experiment);
// TODO(b/323589616): These Has*() functions can be replaced by their
// equivalent internal::CheckStudy* functions once we add the
// corresponding fields to |CreateFilter|.
if (!chosen_experiment && !HasDeviceLevelMismatch(experiment) &&
HasFormFactor(experiment, current_form_factor) &&
HasMinOSVersion(experiment) &&
internal::CheckStudyHardwareClass(filter, hardware_class) &&
IsEnabledForBenchmarking(experiment, is_benchmarking_enabled)) {
chosen_experiment = &experiment;
}
if (experiment.forcing_flag &&
command_line.HasSwitch(experiment.forcing_flag)) {
chosen_experiment = &experiment;
break;
}
}
}
if (chosen_experiment) {
AssociateParamsFromExperiment(study.name, *chosen_experiment, callback,
feature_list);
}
}
} // namespace
std::string EscapeValue(const std::string& value) {
// This needs to be the inverse of UnescapeValue in
// base/metrics/field_trial_params.
std::string net_escaped_str =
base::EscapeQueryParamValue(value, true /* use_plus */);
// net doesn't escape '.' and '*' but base::UnescapeValue() covers those
// cases.
std::string escaped_str;
escaped_str.reserve(net_escaped_str.length());
for (const char ch : net_escaped_str) {
if (ch == '.') {
escaped_str.append("%2E");
} else if (ch == '*') {
escaped_str.append("%2A");
} else {
escaped_str.push_back(ch);
}
}
return escaped_str;
}
bool AssociateParamsFromString(const std::string& varations_string) {
return base::AssociateFieldTrialParamsFromString(varations_string,
&base::UnescapeValue);
}
void AssociateParamsFromFieldTrialConfig(
const FieldTrialTestingConfig& config,
const VariationsSeedProcessor::UIStringOverrideCallback& callback,
Study::Platform platform,
Study::FormFactor current_form_factor,
base::FeatureList* feature_list) {
for (const FieldTrialTestingStudy& study : config.studies) {
CHECK(!study.experiments.empty());
ChooseExperiment(study, callback, platform, current_form_factor,
feature_list);
}
}
void AssociateDefaultFieldTrialConfig(
const VariationsSeedProcessor::UIStringOverrideCallback& callback,
Study::Platform platform,
Study::FormFactor current_form_factor,
base::FeatureList* feature_list) {
AssociateParamsFromFieldTrialConfig(kFieldTrialConfig, callback, platform,
current_form_factor, feature_list);
}
} // namespace variations