diff --git a/rtc_base/experiments/BUILD.gn b/rtc_base/experiments/BUILD.gn index 1ba8827b6b..8b2b0621d8 100644 --- a/rtc_base/experiments/BUILD.gn +++ b/rtc_base/experiments/BUILD.gn @@ -20,6 +20,23 @@ rtc_static_library("alr_experiment") { ] } +rtc_static_library("field_trial_parser") { + sources = [ + "field_trial_parser.cc", + "field_trial_parser.h", + "field_trial_units.cc", + "field_trial_units.h", + ] + deps = [ + "../:rtc_base_approved", + "../../api:optional", + "../../api/units:data_rate", + "../../api/units:data_size", + "../../api/units:time_delta", + "../../system_wrappers:field_trial_api", + ] +} + rtc_static_library("congestion_controller_experiment") { sources = [ "congestion_controller_experiment.cc", @@ -52,13 +69,17 @@ if (rtc_include_tests) { sources = [ "congestion_controller_experiment_unittest.cc", + "field_trial_parser_unittest.cc", + "field_trial_units_unittest.cc", "quality_scaling_experiment_unittest.cc", ] deps = [ ":congestion_controller_experiment", + ":field_trial_parser", ":quality_scaling_experiment", "../:rtc_base_tests_main", "../:rtc_base_tests_utils", + "../../system_wrappers:field_trial_api", "../../test:field_trial", ] } diff --git a/rtc_base/experiments/field_trial_parser.cc b/rtc_base/experiments/field_trial_parser.cc new file mode 100644 index 0000000000..d2a3addae3 --- /dev/null +++ b/rtc_base/experiments/field_trial_parser.cc @@ -0,0 +1,134 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#include "rtc_base/experiments/field_trial_parser.h" + +#include +#include +#include +#include + +#include "rtc_base/logging.h" + +namespace webrtc { +namespace { + +int FindOrEnd(std::string str, size_t start, char delimiter) { + size_t pos = str.find(delimiter, start); + pos = (pos == std::string::npos) ? str.length() : pos; + return static_cast(pos); +} +} // namespace + +FieldTrialParameterInterface::FieldTrialParameterInterface(std::string key) + : key_(key) {} +FieldTrialParameterInterface::~FieldTrialParameterInterface() = default; +std::string FieldTrialParameterInterface::Key() const { + return key_; +} + +void ParseFieldTrial( + std::initializer_list fields, + std::string trial_string) { + std::map field_map; + for (FieldTrialParameterInterface* field : fields) { + field_map[field->Key()] = field; + } + size_t i = 0; + while (i < trial_string.length()) { + int val_end = FindOrEnd(trial_string, i, ','); + int colon_pos = FindOrEnd(trial_string, i, ':'); + int key_end = std::min(val_end, colon_pos); + int val_begin = key_end + 1; + std::string key = trial_string.substr(i, key_end - i); + rtc::Optional opt_value; + if (val_end >= val_begin) + opt_value = trial_string.substr(val_begin, val_end - val_begin); + i = val_end + 1; + auto field = field_map.find(key); + if (field != field_map.end()) { + if (!field->second->Parse(std::move(opt_value))) { + RTC_LOG(LS_WARNING) << "Failed to read field with key: '" << key + << "' in trial: \"" << trial_string << "\""; + } + } else { + RTC_LOG(LS_INFO) << "No field with key: '" << key + << "' (found in trial: \"" << trial_string << "\")"; + } + } +} + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + if (str == "true" || str == "1") { + return true; + } else if (str == "false" || str == "0") { + return false; + } + return rtc::nullopt; +} + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + double value; + if (sscanf(str.c_str(), "%lf", &value) == 1) { + return value; + } else { + return rtc::nullopt; + } +} + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + int value; + if (sscanf(str.c_str(), "%i", &value) == 1) { + return value; + } else { + return rtc::nullopt; + } +} + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + return std::move(str); +} + +FieldTrialFlag::FieldTrialFlag(std::string key) : FieldTrialFlag(key, false) {} + +FieldTrialFlag::FieldTrialFlag(std::string key, bool default_value) + : FieldTrialParameterInterface(key), value_(default_value) {} + +bool FieldTrialFlag::Get() const { + return value_; +} + +bool FieldTrialFlag::Parse(rtc::Optional str_value) { + // Only set the flag if there is no argument provided. + if (str_value) { + rtc::Optional opt_value = ParseTypedParameter(*str_value); + if (!opt_value) + return false; + value_ = *opt_value; + } else { + value_ = true; + } + return true; +} + +template class FieldTrialParameter; +template class FieldTrialParameter; +template class FieldTrialParameter; +template class FieldTrialParameter; + +template class FieldTrialOptional; +template class FieldTrialOptional; +template class FieldTrialOptional; +template class FieldTrialOptional; + +} // namespace webrtc diff --git a/rtc_base/experiments/field_trial_parser.h b/rtc_base/experiments/field_trial_parser.h new file mode 100644 index 0000000000..ba604993c7 --- /dev/null +++ b/rtc_base/experiments/field_trial_parser.h @@ -0,0 +1,146 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#ifndef RTC_BASE_EXPERIMENTS_FIELD_TRIAL_PARSER_H_ +#define RTC_BASE_EXPERIMENTS_FIELD_TRIAL_PARSER_H_ + +#include +#include +#include +#include "api/optional.h" + +// Field trial parser functionality. Provides funcitonality to parse field trial +// argument strings in key:value format. Each parameter is described using +// key:value, parameters are separated with a ,. Values can't include the comma +// character, since there's no quote facility. For most types, white space is +// ignored. Parameters are declared with a given type for which an +// implementation of ParseTypedParameter should be provided. The +// ParseTypedParameter implementation is given whatever is between the : and the +// ,. FieldTrialOptional will use nullopt if the key is provided without :. + +// Example string: "my_optional,my_int:3,my_string:hello" + +// For further description of usage and behavior, see the examples in the unit +// tests. + +namespace webrtc { +class FieldTrialParameterInterface { + public: + virtual ~FieldTrialParameterInterface(); + + protected: + explicit FieldTrialParameterInterface(std::string key); + friend void ParseFieldTrial( + std::initializer_list fields, + std::string raw_string); + virtual bool Parse(rtc::Optional str_value) = 0; + std::string Key() const; + + private: + const std::string key_; +}; + +// ParseFieldTrial function parses the given string and fills the given fields +// with extrated values if available. +void ParseFieldTrial( + std::initializer_list fields, + std::string raw_string); + +// Specialize this in code file for custom types. Should return rtc::nullopt if +// the given string cannot be properly parsed. +template +rtc::Optional ParseTypedParameter(std::string); + +// This class uses the ParseTypedParameter function to implement a parameter +// implementation with an enforced default value. +template +class FieldTrialParameter : public FieldTrialParameterInterface { + public: + FieldTrialParameter(std::string key, T default_value) + : FieldTrialParameterInterface(key), value_(default_value) {} + T Get() const { return value_; } + operator T() const { return Get(); } + + protected: + bool Parse(rtc::Optional str_value) override { + if (str_value) { + rtc::Optional value = ParseTypedParameter(*str_value); + if (value.has_value()) { + value_ = value.value(); + return true; + } + } + return false; + } + + private: + T value_; +}; + +// This class uses the ParseTypedParameter function to implement an optional +// parameter implementation that can default to rtc::nullopt. +template +class FieldTrialOptional : public FieldTrialParameterInterface { + public: + explicit FieldTrialOptional(std::string key) + : FieldTrialParameterInterface(key) {} + FieldTrialOptional(std::string key, rtc::Optional default_value) + : FieldTrialParameterInterface(key), value_(default_value) {} + rtc::Optional Get() const { return value_; } + + protected: + bool Parse(rtc::Optional str_value) override { + if (str_value) { + rtc::Optional value = ParseTypedParameter(*str_value); + if (!value.has_value()) + return false; + value_ = value.value(); + } else { + value_ = rtc::nullopt; + } + return true; + } + + private: + rtc::Optional value_; +}; + +// Equivalent to a FieldTrialParameter in the case that both key and value +// are present. If key is missing, evaluates to false. If key is present, but no +// explicit value is provided, the flag evaluates to true. +class FieldTrialFlag : public FieldTrialParameterInterface { + public: + explicit FieldTrialFlag(std::string key); + FieldTrialFlag(std::string key, bool default_value); + bool Get() const; + + protected: + bool Parse(rtc::Optional str_value) override; + + private: + bool value_; +}; + +// Accepts true, false, else parsed with sscanf %i, true if != 0. +extern template class FieldTrialParameter; +// Interpreted using sscanf %lf. +extern template class FieldTrialParameter; +// Interpreted using sscanf %i. +extern template class FieldTrialParameter; +// Using the given value as is. +extern template class FieldTrialParameter; + +extern template class FieldTrialOptional; +extern template class FieldTrialOptional; +extern template class FieldTrialOptional; +extern template class FieldTrialOptional; + +} // namespace webrtc + +#endif // RTC_BASE_EXPERIMENTS_FIELD_TRIAL_PARSER_H_ diff --git a/rtc_base/experiments/field_trial_parser_unittest.cc b/rtc_base/experiments/field_trial_parser_unittest.cc new file mode 100644 index 0000000000..6c343fc122 --- /dev/null +++ b/rtc_base/experiments/field_trial_parser_unittest.cc @@ -0,0 +1,131 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#include "rtc_base/experiments/field_trial_parser.h" +#include "rtc_base/gunit.h" +#include "system_wrappers/include/field_trial.h" +#include "test/field_trial.h" + +namespace webrtc { +namespace { +const char kDummyExperiment[] = "WebRTC-DummyExperiment"; + +struct DummyExperiment { + FieldTrialFlag enabled = FieldTrialFlag("Enabled"); + FieldTrialParameter factor = FieldTrialParameter("f", 0.5); + FieldTrialParameter retries = FieldTrialParameter("r", 5); + FieldTrialParameter ping = FieldTrialParameter("p", 0); + FieldTrialParameter hash = + FieldTrialParameter("h", "a80"); + + explicit DummyExperiment(std::string field_trial) { + ParseFieldTrial({&enabled, &factor, &retries, &ping, &hash}, field_trial); + } + DummyExperiment() { + std::string trial_string = field_trial::FindFullName(kDummyExperiment); + ParseFieldTrial({&enabled, &factor, &retries, &ping, &hash}, trial_string); + } +}; + +enum class CustomEnum { + kDefault, + kRed, + kBlue, +}; +} // namespace + +// Providing a custom parser for an enum can make the trial string easier to +// read, but also adds more code and makes the string more verbose. +template <> +rtc::Optional ParseTypedParameter(std::string str) { + if (str == "default") + return CustomEnum::kDefault; + else if (str == "red") + return CustomEnum::kRed; + else if (str == "blue") + return CustomEnum::kBlue; + return rtc::nullopt; +} + +TEST(FieldTrialParserTest, ParsesValidParameters) { + DummyExperiment exp("Enabled,f:-1.7,r:2,p:1,h:x7c"); + EXPECT_TRUE(exp.enabled.Get()); + EXPECT_EQ(exp.factor.Get(), -1.7); + EXPECT_EQ(exp.retries.Get(), 2); + EXPECT_EQ(exp.ping.Get(), true); + EXPECT_EQ(exp.hash.Get(), "x7c"); +} +TEST(FieldTrialParserTest, InitializesFromFieldTrial) { + test::ScopedFieldTrials field_trials( + "WebRTC-OtherExperiment/Disabled/" + "WebRTC-DummyExperiment/Enabled,f:-1.7,r:2,p:1,h:x7c/" + "WebRTC-AnotherExperiment/Enabled,f:-3.1,otherstuff:beef/"); + DummyExperiment exp; + EXPECT_TRUE(exp.enabled.Get()); + EXPECT_EQ(exp.factor.Get(), -1.7); + EXPECT_EQ(exp.retries.Get(), 2); + EXPECT_EQ(exp.ping.Get(), true); + EXPECT_EQ(exp.hash.Get(), "x7c"); +} +TEST(FieldTrialParserTest, UsesDefaults) { + DummyExperiment exp(""); + EXPECT_FALSE(exp.enabled.Get()); + EXPECT_EQ(exp.factor.Get(), 0.5); + EXPECT_EQ(exp.retries.Get(), 5); + EXPECT_EQ(exp.ping.Get(), false); + EXPECT_EQ(exp.hash.Get(), "a80"); +} +TEST(FieldTrialParserTest, CanHandleMixedInput) { + DummyExperiment exp("p:true,h:,Enabled"); + EXPECT_TRUE(exp.enabled.Get()); + EXPECT_EQ(exp.factor.Get(), 0.5); + EXPECT_EQ(exp.retries.Get(), 5); + EXPECT_EQ(exp.ping.Get(), true); + EXPECT_EQ(exp.hash.Get(), ""); +} +TEST(FieldTrialParserTest, IgnoresNewKey) { + DummyExperiment exp("Disabled,r:-11,foo"); + EXPECT_FALSE(exp.enabled.Get()); + EXPECT_EQ(exp.factor.Get(), 0.5); + EXPECT_EQ(exp.retries.Get(), -11); +} +TEST(FieldTrialParserTest, IgnoresInvalid) { + DummyExperiment exp("Enabled,f,p:,r:%,,:foo,h"); + EXPECT_TRUE(exp.enabled.Get()); + EXPECT_EQ(exp.factor.Get(), 0.5); + EXPECT_EQ(exp.retries.Get(), 5); + EXPECT_EQ(exp.ping.Get(), false); + EXPECT_EQ(exp.hash.Get(), "a80"); +} +TEST(FieldTrialParserTest, ParsesOptionalParameters) { + FieldTrialOptional max_count("c", rtc::nullopt); + ParseFieldTrial({&max_count}, ""); + EXPECT_FALSE(max_count.Get().has_value()); + ParseFieldTrial({&max_count}, "c:10"); + EXPECT_EQ(max_count.Get().value(), 10); + ParseFieldTrial({&max_count}, "c"); + EXPECT_FALSE(max_count.Get().has_value()); + ParseFieldTrial({&max_count}, "c:20"); + EXPECT_EQ(max_count.Get().value(), 20); + ParseFieldTrial({&max_count}, "c:"); + EXPECT_EQ(max_count.Get().value(), 20); + FieldTrialOptional optional_string("s", std::string("ab")); + ParseFieldTrial({&optional_string}, "s:"); + EXPECT_EQ(optional_string.Get().value(), ""); + ParseFieldTrial({&optional_string}, "s"); + EXPECT_FALSE(optional_string.Get().has_value()); +} +TEST(FieldTrialParserTest, ParsesCustomEnumParameter) { + FieldTrialParameter my_enum("e", CustomEnum::kDefault); + ParseFieldTrial({&my_enum}, ""); + EXPECT_EQ(my_enum.Get(), CustomEnum::kDefault); + ParseFieldTrial({&my_enum}, "e:red"); + EXPECT_EQ(my_enum.Get(), CustomEnum::kRed); +} +} // namespace webrtc diff --git a/rtc_base/experiments/field_trial_units.cc b/rtc_base/experiments/field_trial_units.cc new file mode 100644 index 0000000000..94af4a72b3 --- /dev/null +++ b/rtc_base/experiments/field_trial_units.cc @@ -0,0 +1,92 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#include "rtc_base/experiments/field_trial_units.h" + +#include +#include + +#include "api/optional.h" + +// Large enough to fit "seconds", the longest supported unit name. +#define RTC_TRIAL_UNIT_LENGTH_STR "7" +#define RTC_TRIAL_UNIT_SIZE 8 + +namespace webrtc { +namespace { + +struct ValueWithUnit { + double value; + std::string unit; +}; + +rtc::Optional ParseValueWithUnit(std::string str) { + if (str == "inf") { + return ValueWithUnit{std::numeric_limits::infinity(), ""}; + } else if (str == "-inf") { + return ValueWithUnit{-std::numeric_limits::infinity(), ""}; + } else { + double double_val; + char unit_char[RTC_TRIAL_UNIT_SIZE]; + unit_char[0] = 0; + if (sscanf(str.c_str(), "%lf%" RTC_TRIAL_UNIT_LENGTH_STR "s", &double_val, + unit_char) >= 1) { + return ValueWithUnit{double_val, unit_char}; + } + } + return rtc::nullopt; +} +} // namespace + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + rtc::Optional result = ParseValueWithUnit(str); + if (result) { + if (result->unit.empty() || result->unit == "kbps") { + return DataRate::kbps(result->value); + } else if (result->unit == "bps") { + return DataRate::bps(result->value); + } + } + return rtc::nullopt; +} + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + rtc::Optional result = ParseValueWithUnit(str); + if (result) { + if (result->unit.empty() || result->unit == "bytes") + return DataSize::bytes(result->value); + } + return rtc::nullopt; +} + +template <> +rtc::Optional ParseTypedParameter(std::string str) { + rtc::Optional result = ParseValueWithUnit(str); + if (result) { + if (result->unit == "s" || result->unit == "seconds") { + return TimeDelta::seconds(result->value); + } else if (result->unit == "us") { + return TimeDelta::us(result->value); + } else if (result->unit.empty() || result->unit == "ms") { + return TimeDelta::ms(result->value); + } + } + return rtc::nullopt; +} + +template class FieldTrialParameter; +template class FieldTrialParameter; +template class FieldTrialParameter; + +template class FieldTrialOptional; +template class FieldTrialOptional; +template class FieldTrialOptional; +} // namespace webrtc diff --git a/rtc_base/experiments/field_trial_units.h b/rtc_base/experiments/field_trial_units.h new file mode 100644 index 0000000000..932c5cb450 --- /dev/null +++ b/rtc_base/experiments/field_trial_units.h @@ -0,0 +1,29 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#ifndef RTC_BASE_EXPERIMENTS_FIELD_TRIAL_UNITS_H_ +#define RTC_BASE_EXPERIMENTS_FIELD_TRIAL_UNITS_H_ + +#include "rtc_base/experiments/field_trial_parser.h" + +#include "api/units/data_rate.h" +#include "api/units/data_size.h" +#include "api/units/time_delta.h" + +namespace webrtc { +extern template class FieldTrialParameter; +extern template class FieldTrialParameter; +extern template class FieldTrialParameter; + +extern template class FieldTrialOptional; +extern template class FieldTrialOptional; +extern template class FieldTrialOptional; +} // namespace webrtc + +#endif // RTC_BASE_EXPERIMENTS_FIELD_TRIAL_UNITS_H_ diff --git a/rtc_base/experiments/field_trial_units_unittest.cc b/rtc_base/experiments/field_trial_units_unittest.cc new file mode 100644 index 0000000000..da76f5ad89 --- /dev/null +++ b/rtc_base/experiments/field_trial_units_unittest.cc @@ -0,0 +1,62 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#include + +#include "rtc_base/experiments/field_trial_parser.h" +#include "rtc_base/experiments/field_trial_units.h" +#include "rtc_base/gunit.h" + +namespace webrtc { +namespace { +struct DummyExperiment { + FieldTrialParameter target_rate = + FieldTrialParameter("t", DataRate::kbps(100)); + FieldTrialParameter period = + FieldTrialParameter("p", TimeDelta::ms(100)); + FieldTrialOptional max_buffer = + FieldTrialOptional("b", rtc::nullopt); + + explicit DummyExperiment(std::string field_trial) { + ParseFieldTrial({&target_rate, &max_buffer, &period}, field_trial); + } +}; +} // namespace + +TEST(FieldTrialParserUnitsTest, FallsBackToDefaults) { + DummyExperiment exp(""); + EXPECT_EQ(exp.target_rate.Get(), DataRate::kbps(100)); + EXPECT_FALSE(exp.max_buffer.Get().has_value()); + EXPECT_EQ(exp.period.Get(), TimeDelta::ms(100)); +} +TEST(FieldTrialParserUnitsTest, ParsesUnitParameters) { + DummyExperiment exp("t:300kbps,b:5bytes,p:300ms"); + EXPECT_EQ(exp.target_rate.Get(), DataRate::kbps(300)); + EXPECT_EQ(*exp.max_buffer.Get(), DataSize::bytes(5)); + EXPECT_EQ(exp.period.Get(), TimeDelta::ms(300)); +} +TEST(FieldTrialParserUnitsTest, ParsesDefaultUnitParameters) { + DummyExperiment exp("t:300,b:5,p:300"); + EXPECT_EQ(exp.target_rate.Get(), DataRate::kbps(300)); + EXPECT_EQ(*exp.max_buffer.Get(), DataSize::bytes(5)); + EXPECT_EQ(exp.period.Get(), TimeDelta::ms(300)); +} +TEST(FieldTrialParserUnitsTest, ParsesInfinityParameter) { + DummyExperiment exp("t:inf,p:inf"); + EXPECT_EQ(exp.target_rate.Get(), DataRate::Infinity()); + EXPECT_EQ(exp.period.Get(), TimeDelta::PlusInfinity()); +} +TEST(FieldTrialParserUnitsTest, ParsesOtherUnitParameters) { + DummyExperiment exp("t:300bps,p:0.3 seconds,b:8 bytes"); + EXPECT_EQ(exp.target_rate.Get(), DataRate::bps(300)); + EXPECT_EQ(*exp.max_buffer.Get(), DataSize::bytes(8)); + EXPECT_EQ(exp.period.Get(), TimeDelta::ms(300)); +} + +} // namespace webrtc