| // Copyright 2017 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 "chromecast/base/cast_features.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/field_trial_param_associator.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/no_destructor.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| |
| namespace chromecast { |
| namespace { |
| // A constant used to always activate a FieldTrial. |
| const base::FieldTrial::Probability k100PercentProbability = 100; |
| |
| // The name of the default group to use for Cast DCS features. |
| const char kDefaultDCSFeaturesGroup[] = "default_dcs_features_group"; |
| |
| std::unordered_set<int32_t>& GetExperimentIds() { |
| static base::NoDestructor<std::unordered_set<int32_t>> g_experiment_ids; |
| return *g_experiment_ids; |
| } |
| |
| bool g_experiment_ids_initialized = false; |
| |
| // The collection of features that have been registered by unit tests |
| std::vector<const base::Feature*>& GetTestFeatures() { |
| static base::NoDestructor<std::vector<const base::Feature*>> |
| features_for_test; |
| return *features_for_test; |
| } |
| |
| void SetExperimentIds(const base::ListValue& list) { |
| DCHECK(!g_experiment_ids_initialized); |
| std::unordered_set<int32_t> ids; |
| for (size_t i = 0; i < list.GetSize(); ++i) { |
| int32_t id; |
| if (list.GetInteger(i, &id)) { |
| ids.insert(id); |
| } else { |
| LOG(ERROR) << "Non-integer value found in experiment id list!"; |
| } |
| } |
| GetExperimentIds().swap(ids); |
| g_experiment_ids_initialized = true; |
| } |
| |
| } // namespace |
| |
| // PLEASE READ! |
| // Cast Platform Features are listed below. These features may be |
| // toggled via configs fetched from DCS for devices in the field, or via |
| // command-line flags set by the developer. For the end-to-end details of the |
| // system design, please see go/dcs-experiments. |
| // |
| // Below are useful steps on how to use these features in your code. |
| // |
| // 1) Declaring and defining the feature. |
| // All Cast Platform Features should be declared in a common file with other |
| // Cast Platform Features (ex. chromecast/base/cast_features.h). When |
| // defining your feature, you will need to assign a default value. This is |
| // the value that the feature will hold until overriden by the server or the |
| // command line. Here's an exmaple: |
| // |
| // const base::Feature kSuperSecretSauce{ |
| // "enable_super_secret_sauce", base::FEATURE_DISABLED_BY_DEFAULT}; |
| // |
| // IMPORTANT NOTE: |
| // The first parameter that you pass in the definition is the feature's name. |
| // This MUST match the DCS experiment key for this feature. |
| // |
| // While Features elsewhere in Chromium alternatively use dashed-case or |
| // PascalCase for their names, Chromecast features should use snake_case |
| // (lowercase letters separated by underscores). This will ensure that DCS |
| // configs, which are passed around as JSON, remain conformant and readable. |
| // |
| // 2) Using the feature in client code. |
| // Using these features in your code is easy. Here's an example: |
| // |
| // #include “base/feature_list.h” |
| // #include “chromecast/base/chromecast_switches.h” |
| // |
| // std::unique_ptr<Foo> CreateFoo() { |
| // if (base::FeatureList::IsEnabled(kSuperSecretSauce)) |
| // return std::make_unique<SuperSecretFoo>(); |
| // return std::make_unique<BoringOldFoo>(); |
| // } |
| // |
| // base::FeatureList can be called from any thread, in any process, at any |
| // time after PreCreateThreads(). It will return whether the feature is |
| // enabled. |
| // |
| // 3) Overriding the default value from the server. |
| // For devices in the field, DCS will issue different configs to different |
| // groups of devices, allowing us to run experiments on features. These |
| // feature settings will manifest on the next boot of cast_shell. In the |
| // example, if the latest config for a particular device set the value of |
| // kSuperSecretSauce to true, the appropriate code path would be taken. |
| // Otherwise, the default value, false, would be used. For more details on |
| // setting up experiments, see go/dcs-launch. |
| // |
| // 4) Overriding the default and server values from the command-line. |
| // While the server value trumps the default values, the command line trumps |
| // both. Enable features by passing this command line arg to cast_shell: |
| // |
| // --enable-features=enable_foo,enable_super_secret_sauce |
| // |
| // Features are separated by commas. Disable features by passing: |
| // |
| // --disable-features=enable_foo,enable_bar |
| // |
| // 5) If you add a new feature to the system you must include it in kFeatures |
| // This is because the system relies on knowing all of the features so |
| // it can properly iterate over all features to detect changes. |
| // |
| |
| // Begin Chromecast Feature definitions. |
| |
| // Allows applications to access media capture devices (webcams/microphones) |
| // through getUserMedia API. |
| const base::Feature kAllowUserMediaAccess{"allow_user_media_access", |
| base::FEATURE_DISABLED_BY_DEFAULT}; |
| // Enables the use of QUIC in Cast-specific URLRequestContextGetters. See |
| // chromecast/browser/url_request_context_factory.cc for usage. |
| const base::Feature kEnableQuic{"enable_quic", |
| base::FEATURE_DISABLED_BY_DEFAULT}; |
| // Enables triple-buffer 720p graphics (overriding default graphics buffer |
| // settings for a platform). |
| const base::Feature kTripleBuffer720{"enable_triple_buffer_720", |
| base::FEATURE_DISABLED_BY_DEFAULT}; |
| // Enables single-buffered graphics (overriding default graphics buffer |
| // settings and takes precedence over triple-buffer feature). |
| const base::Feature kSingleBuffer{"enable_single_buffer", |
| base::FEATURE_DISABLED_BY_DEFAULT}; |
| // Disable idle sockets closing on memory pressure. See |
| // chromecast/browser/url_request_context_factory.cc for usage. |
| const base::Feature kDisableIdleSocketsCloseOnMemoryPressure{ |
| "disable_idle_sockets_close_on_memory_pressure", |
| base::FEATURE_DISABLED_BY_DEFAULT}; |
| |
| const base::Feature kEnableGeneralAudienceBrowsing{ |
| "enable_general_audience_browsing", base::FEATURE_DISABLED_BY_DEFAULT}; |
| |
| // End Chromecast Feature definitions. |
| const base::Feature* kFeatures[] = { |
| &kAllowUserMediaAccess, |
| &kEnableQuic, |
| &kTripleBuffer720, |
| &kSingleBuffer, |
| &kDisableIdleSocketsCloseOnMemoryPressure, |
| &kEnableGeneralAudienceBrowsing, |
| }; |
| |
| // An iterator for a base::DictionaryValue. Use an alias for brevity in loops. |
| using Iterator = base::DictionaryValue::Iterator; |
| |
| std::vector<const base::Feature*> GetInternalFeatures(); |
| |
| const std::vector<const base::Feature*>& GetFeatures() { |
| static const base::NoDestructor<std::vector<const base::Feature*>> features( |
| [] { |
| auto features = std::vector<const base::Feature*>( |
| kFeatures, kFeatures + sizeof(kFeatures) / sizeof(base::Feature*)); |
| auto internal_features = GetInternalFeatures(); |
| features.insert(features.end(), internal_features.begin(), |
| internal_features.end()); |
| return features; |
| }()); |
| if (GetTestFeatures().size() > 0) |
| return GetTestFeatures(); |
| return *features; |
| } |
| |
| void InitializeFeatureList(const base::DictionaryValue& dcs_features, |
| const base::ListValue& dcs_experiment_ids, |
| const std::string& cmd_line_enable_features, |
| const std::string& cmd_line_disable_features, |
| const std::string& extra_enable_features, |
| const std::string& extra_disable_features) { |
| DCHECK(!base::FeatureList::GetInstance()); |
| |
| // Set the experiments. |
| SetExperimentIds(dcs_experiment_ids); |
| |
| std::string all_enable_features = |
| cmd_line_enable_features + "," + extra_enable_features; |
| std::string all_disable_features = |
| cmd_line_disable_features + "," + extra_disable_features; |
| |
| // Initialize the FeatureList from the command line. |
| auto feature_list = std::make_unique<base::FeatureList>(); |
| feature_list->InitializeFromCommandLine(all_enable_features, |
| all_disable_features); |
| |
| // Override defaults from the DCS config. |
| for (Iterator it(dcs_features); !it.IsAtEnd(); it.Advance()) { |
| // Each feature must have its own FieldTrial object. Since experiments are |
| // controlled server-side for Chromecast, and this class is designed with a |
| // client-side experimentation framework in mind, these parameters are |
| // carefully chosen: |
| // - The field trial name is unused for our purposes. However, we need to |
| // maintain a 1:1 mapping with Features in order to properly store and |
| // access parameters associated with each Feature. Therefore, use the |
| // Feature's name as the FieldTrial name to ensure uniqueness. |
| // - The probability is hard-coded to 100% so that the FeatureList always |
| // respects the value from DCS. |
| // - The default group is unused; it will be the same for every feature. |
| // - Expiration year, month, and day use a special value such that the |
| // feature will never expire. |
| // - SESSION_RANDOMIZED is used to prevent the need for an |
| // entropy_provider. However, this value doesn't matter. |
| // - We don't care about the group_id. |
| // |
| const std::string& feature_name = it.key(); |
| auto* field_trial = base::FieldTrialList::FactoryGetFieldTrial( |
| feature_name, k100PercentProbability, kDefaultDCSFeaturesGroup, |
| base::FieldTrialList::kNoExpirationYear, 1 /* month */, 1 /* day */, |
| base::FieldTrial::SESSION_RANDOMIZED, nullptr); |
| |
| bool enabled; |
| if (it.value().GetAsBoolean(&enabled)) { |
| // A boolean entry simply either enables or disables a feature. |
| feature_list->RegisterFieldTrialOverride( |
| feature_name, |
| enabled ? base::FeatureList::OVERRIDE_ENABLE_FEATURE |
| : base::FeatureList::OVERRIDE_DISABLE_FEATURE, |
| field_trial); |
| continue; |
| } |
| |
| const base::DictionaryValue* params_dict; |
| if (it.value().GetAsDictionary(¶ms_dict)) { |
| // A dictionary entry implies that the feature is enabled. |
| feature_list->RegisterFieldTrialOverride( |
| feature_name, base::FeatureList::OVERRIDE_ENABLE_FEATURE, |
| field_trial); |
| |
| // If the feature has not been overriden from the command line, set its |
| // parameters accordingly. |
| if (!feature_list->IsFeatureOverriddenFromCommandLine( |
| feature_name, base::FeatureList::OVERRIDE_DISABLE_FEATURE)) { |
| // Build a map of the FieldTrial parameters and associate it to the |
| // FieldTrial. |
| base::FieldTrialParamAssociator::FieldTrialParams params; |
| for (Iterator p(*params_dict); !p.IsAtEnd(); p.Advance()) { |
| std::string val; |
| if (p.value().GetAsString(&val)) { |
| params[p.key()] = val; |
| } else { |
| LOG(ERROR) << "Entry in params dict for \"" << feature_name << "\"" |
| << " feature is not a string. Skipping."; |
| } |
| } |
| |
| // Register the params, so that they can be queried by client code. |
| bool success = base::AssociateFieldTrialParams( |
| feature_name, kDefaultDCSFeaturesGroup, params); |
| DCHECK(success); |
| } |
| continue; |
| } |
| |
| // Other base::Value types are not supported. |
| LOG(ERROR) << "A DCS feature mapped to an unsupported value. key: " |
| << feature_name << " type: " << it.value().type(); |
| } |
| |
| base::FeatureList::SetInstance(std::move(feature_list)); |
| } |
| |
| bool IsFeatureEnabled(const base::Feature& feature) { |
| DCHECK(base::ContainsValue(GetFeatures(), &feature)) << feature.name; |
| return base::FeatureList::IsEnabled(feature); |
| } |
| |
| base::DictionaryValue GetOverriddenFeaturesForStorage( |
| const base::Value& features) { |
| base::DictionaryValue persistent_dict; |
| |
| // |features| maps feature names to either a boolean or a dict of params. |
| for (const auto& feature : features.DictItems()) { |
| if (feature.second.is_bool()) { |
| persistent_dict.SetBoolean(feature.first, feature.second.GetBool()); |
| continue; |
| } |
| |
| const base::DictionaryValue* params_dict; |
| if (feature.second.GetAsDictionary(¶ms_dict)) { |
| auto params = std::make_unique<base::DictionaryValue>(); |
| |
| bool bval; |
| int ival; |
| double dval; |
| std::string sval; |
| for (Iterator p(*params_dict); !p.IsAtEnd(); p.Advance()) { |
| const auto& param_key = p.key(); |
| const auto& param_val = p.value(); |
| if (param_val.GetAsBoolean(&bval)) { |
| params->SetString(param_key, bval ? "true" : "false"); |
| } else if (param_val.GetAsInteger(&ival)) { |
| params->SetString(param_key, base::NumberToString(ival)); |
| } else if (param_val.GetAsDouble(&dval)) { |
| params->SetString(param_key, base::NumberToString(dval)); |
| } else if (param_val.GetAsString(&sval)) { |
| params->SetString(param_key, sval); |
| } else { |
| LOG(ERROR) << "Entry in params dict for \"" << feature.first << "\"" |
| << " is not of a supported type (key: " << param_key |
| << ", type: " << param_val.type(); |
| } |
| } |
| persistent_dict.Set(feature.first, std::move(params)); |
| continue; |
| } |
| |
| // Other base::Value types are not supported. |
| LOG(ERROR) << "A DCS feature mapped to an unsupported value. key: " |
| << feature.first << " type: " << feature.second.type(); |
| } |
| |
| return persistent_dict; |
| } |
| |
| const std::unordered_set<int32_t>& GetDCSExperimentIds() { |
| DCHECK(g_experiment_ids_initialized); |
| return GetExperimentIds(); |
| } |
| |
| void ResetCastFeaturesForTesting() { |
| g_experiment_ids_initialized = false; |
| base::FeatureList::ClearInstanceForTesting(); |
| GetTestFeatures().clear(); |
| } |
| |
| void SetFeaturesForTest(std::vector<const base::Feature*> features) { |
| GetTestFeatures() = std::move(features); |
| } |
| |
| } // namespace chromecast |