1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
|
// 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
|