From 97d84ef78e5bea6b72d3cca2a2b7a7521da12d45 Mon Sep 17 00:00:00 2001 From: Jonas Olsson Date: Thu, 11 Apr 2019 11:53:26 +0200 Subject: [PATCH] Add support for lists to the FieldTrialParser. List elements are separated by a |. If the key is given without a : we treat that as a empty list. We also support parsing multiple lists as a list-of-structs, see the unit test for usage examples. Bug: webrtc:9346 Change-Id: I32d3ce612fef476b1c481c00a893d7fa2f339e92 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/130464 Reviewed-by: Sebastian Jansson Commit-Queue: Jonas Olsson Cr-Commit-Position: refs/heads/master@{#27560} --- rtc_base/experiments/BUILD.gn | 4 + rtc_base/experiments/field_trial_list.cc | 57 +++++ rtc_base/experiments/field_trial_list.h | 224 ++++++++++++++++++ .../experiments/field_trial_list_unittest.cc | 133 +++++++++++ rtc_base/experiments/field_trial_parser.cc | 23 +- rtc_base/experiments/field_trial_parser.h | 13 +- .../field_trial_parser_unittest.cc | 5 + 7 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 rtc_base/experiments/field_trial_list.cc create mode 100644 rtc_base/experiments/field_trial_list.h create mode 100644 rtc_base/experiments/field_trial_list_unittest.cc diff --git a/rtc_base/experiments/BUILD.gn b/rtc_base/experiments/BUILD.gn index b8709dc25b..70dcb5bf36 100644 --- a/rtc_base/experiments/BUILD.gn +++ b/rtc_base/experiments/BUILD.gn @@ -40,6 +40,8 @@ rtc_static_library("audio_allocation_settings") { rtc_static_library("field_trial_parser") { sources = [ + "field_trial_list.cc", + "field_trial_list.h", "field_trial_parser.cc", "field_trial_parser.h", "field_trial_units.cc", @@ -51,6 +53,7 @@ rtc_static_library("field_trial_parser") { "../../api/units:time_delta", "../../rtc_base:checks", "../../rtc_base:logging", + "../../rtc_base:stringutils", "//third_party/abseil-cpp/absl/types:optional", ] } @@ -151,6 +154,7 @@ if (rtc_include_tests) { sources = [ "cpu_speed_experiment_unittest.cc", + "field_trial_list_unittest.cc", "field_trial_parser_unittest.cc", "field_trial_units_unittest.cc", "keyframe_interval_settings_unittest.cc", diff --git a/rtc_base/experiments/field_trial_list.cc b/rtc_base/experiments/field_trial_list.cc new file mode 100644 index 0000000000..de897bfbef --- /dev/null +++ b/rtc_base/experiments/field_trial_list.cc @@ -0,0 +1,57 @@ +/* + * Copyright 2019 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_list.h" + +namespace webrtc { + +FieldTrialListBase::FieldTrialListBase(std::string key) + : FieldTrialParameterInterface(key), + failed_(false), + parse_got_called_(false) {} + +bool FieldTrialListBase::Failed() const { + return failed_; +} +bool FieldTrialListBase::Used() const { + return parse_got_called_; +} + +int FieldTrialListWrapper::Length() { + return GetList()->Size(); +} +bool FieldTrialListWrapper::Failed() { + return GetList()->Failed(); +} +bool FieldTrialListWrapper::Used() { + return GetList()->Used(); +} + +bool FieldTrialStructListBase::Parse(absl::optional str_value) { + RTC_NOTREACHED(); + return true; +} + +int FieldTrialStructListBase::ValidateAndGetLength() { + int length = -1; + for (std::unique_ptr& list : sub_lists_) { + if (list->Failed()) + return -1; + else if (!list->Used()) + continue; + else if (length == -1) + length = list->Length(); + else if (length != list->Length()) + return -1; + } + + return length; +} + +} // namespace webrtc diff --git a/rtc_base/experiments/field_trial_list.h b/rtc_base/experiments/field_trial_list.h new file mode 100644 index 0000000000..cfbcce9a64 --- /dev/null +++ b/rtc_base/experiments/field_trial_list.h @@ -0,0 +1,224 @@ +/* + * 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_LIST_H_ +#define RTC_BASE_EXPERIMENTS_FIELD_TRIAL_LIST_H_ + +#include +#include +#include +#include + +#include "rtc_base/experiments/field_trial_parser.h" +#include "rtc_base/string_encode.h" + +// List support for field trial strings. FieldTrialList and FieldTrialStructList +// are used similarly to the other FieldTrialParameters, but take a variable +// number of parameters. A FieldTrialList parses a |-delimeted string into a +// list of T, using ParseTypedParameter to parse the individual tokens. +// Example string: "my_list:1|2|3,empty_list,other_list:aardvark". + +// A FieldTrialStructList combines multiple lists into a list-of-structs. It +// ensures that all its sublists parse correctly and have the same length, then +// uses user-supplied accessor functions to write those elements into structs of +// a user-supplied type. + +// See the unit test for usage and behavior. + +namespace webrtc { + +class FieldTrialListBase : public FieldTrialParameterInterface { + protected: + friend class FieldTrialListWrapper; + explicit FieldTrialListBase(std::string key); + + bool Failed() const; + bool Used() const; + + virtual int Size() = 0; + + bool failed_; + bool parse_got_called_; +}; + +// This class represents a vector of type T. The elements are separated by a | +// and parsed using ParseTypedParameter. +template +class FieldTrialList : public FieldTrialListBase { + public: + explicit FieldTrialList(std::string key) : FieldTrialList(key, {}) {} + FieldTrialList(std::string key, std::initializer_list default_values) + : FieldTrialListBase(key), values_(default_values) {} + + std::vector Get() const { return values_; } + operator std::vector() const { return Get(); } + const T& operator[](size_t index) const { return values_[index]; } + const std::vector* operator->() const { return &values_; } + + protected: + bool Parse(absl::optional str_value) override { + parse_got_called_ = true; + + if (!str_value) { + values_.clear(); + return true; + } + + std::vector tokens; + std::vector new_values_; + rtc::split(str_value.value(), '|', &tokens); + + for (std::string token : tokens) { + absl::optional value = ParseTypedParameter(token); + if (value) { + new_values_.push_back(*value); + } else { + failed_ = true; + return false; + } + } + + values_.swap(new_values_); + return true; + } + + int Size() override { return values_.size(); } + + private: + std::vector values_; +}; + +class FieldTrialListWrapper { + public: + virtual ~FieldTrialListWrapper() = default; + + // Takes the element at the given index in the wrapped list and writes it to + // the given struct. + virtual void WriteElement(void* struct_to_write, int index) = 0; + + virtual FieldTrialListBase* GetList() = 0; + + int Length(); + + // Returns true iff the wrapped list has failed to parse at least one token. + bool Failed(); + + bool Used(); + + protected: + FieldTrialListWrapper() = default; +}; + +namespace field_trial_list_impl { +// The LambdaTypeTraits struct provides type information about lambdas in the +// template expressions below. +template +struct LambdaTypeTraits : public LambdaTypeTraits {}; + +template +struct LambdaTypeTraits { + using ret = RetType; + using src = SourceType; +}; + +template +struct TypedFieldTrialListWrapper : FieldTrialListWrapper { + public: + TypedFieldTrialListWrapper(std::string key, + std::function sink) + : list_(key), sink_(sink) {} + + void WriteElement(void* struct_to_write, int index) override { + sink_(struct_to_write, list_[index]); + } + + FieldTrialListBase* GetList() { return &list_; } + + private: + FieldTrialList list_; + std::function sink_; +}; + +} // namespace field_trial_list_impl + +template > +FieldTrialListWrapper* FieldTrialStructMember(std::string key, F accessor) { + return new field_trial_list_impl::TypedFieldTrialListWrapper< + typename Traits::ret>(key, [accessor](void* s, typename Traits::ret t) { + *accessor(static_cast(s)) = t; + }); +} + +// This base class is here to reduce the amount of code we have to generate for +// each type of FieldTrialStructList. +class FieldTrialStructListBase : public FieldTrialParameterInterface { + protected: + FieldTrialStructListBase( + std::initializer_list sub_lists) + : FieldTrialParameterInterface(""), sub_lists_() { + // Take ownership of the list wrappers generated by FieldTrialStructMember + // on the call site. + for (FieldTrialListWrapper* const* it = sub_lists.begin(); + it != sub_lists.end(); it++) { + sub_parameters_.push_back((*it)->GetList()); + sub_lists_.push_back(std::unique_ptr(*it)); + } + } + + // Check that all of our sublists that were in the field trial string had the + // same number of elements. If they do, we return that length. If they had + // different lengths, any sublist had parse failures or no sublists had + // user-supplied values, we return -1. + int ValidateAndGetLength(); + + bool Parse(absl::optional str_value) override; + + std::vector> sub_lists_; +}; + +template +class FieldTrialStructList : public FieldTrialStructListBase { + public: + FieldTrialStructList(std::initializer_list l, + std::initializer_list default_list) + : FieldTrialStructListBase(l), values_(default_list) {} + + std::vector Get() const { return values_; } + operator std::vector() const { return Get(); } + const S& operator[](size_t index) const { return values_[index]; } + const std::vector* operator->() const { return &values_; } + + protected: + void ParseDone() override { + int length = ValidateAndGetLength(); + + if (length == -1) + return; + + std::vector new_values(length, S()); + + for (std::unique_ptr& li : sub_lists_) { + if (li->Used()) { + for (int i = 0; i < length; i++) { + li->WriteElement(&new_values[i], i); + } + } + } + + values_.swap(new_values); + } + + private: + std::vector values_; +}; + +} // namespace webrtc + +#endif // RTC_BASE_EXPERIMENTS_FIELD_TRIAL_LIST_H_ diff --git a/rtc_base/experiments/field_trial_list_unittest.cc b/rtc_base/experiments/field_trial_list_unittest.cc new file mode 100644 index 0000000000..a1abfe4bf8 --- /dev/null +++ b/rtc_base/experiments/field_trial_list_unittest.cc @@ -0,0 +1,133 @@ +/* + * Copyright 2019 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_list.h" + +#include "rtc_base/gunit.h" +#include "test/gmock.h" + +using testing::ElementsAre; +using testing::IsEmpty; + +namespace webrtc { + +struct Garment { + int price = 0; + std::string color = ""; + + // Only needed for testing. + Garment() = default; + Garment(int p, std::string c) : price(p), color(c) {} + + bool operator==(const Garment& other) const { + return price == other.price && color == other.color; + } +}; + +TEST(FieldTrialListTest, ParsesListParameter) { + FieldTrialList my_list("l", {5}); + EXPECT_THAT(my_list.Get(), ElementsAre(5)); + // If one element is invalid the list is unchanged. + ParseFieldTrial({&my_list}, "l:1|2|hat"); + EXPECT_THAT(my_list.Get(), ElementsAre(5)); + ParseFieldTrial({&my_list}, "l"); + EXPECT_THAT(my_list.Get(), IsEmpty()); + ParseFieldTrial({&my_list}, "l:1|2|3"); + EXPECT_THAT(my_list.Get(), ElementsAre(1, 2, 3)); + ParseFieldTrial({&my_list}, "l:-1"); + EXPECT_THAT(my_list.Get(), ElementsAre(-1)); + + FieldTrialList another_list("l", {"hat"}); + EXPECT_THAT(another_list.Get(), ElementsAre("hat")); + ParseFieldTrial({&another_list}, "l"); + EXPECT_THAT(another_list.Get(), IsEmpty()); + ParseFieldTrial({&another_list}, "l:"); + EXPECT_THAT(another_list.Get(), ElementsAre("")); + ParseFieldTrial({&another_list}, "l:scarf|hat|mittens"); + EXPECT_THAT(another_list.Get(), ElementsAre("scarf", "hat", "mittens")); + ParseFieldTrial({&another_list}, "l:scarf"); + EXPECT_THAT(another_list.Get(), ElementsAre("scarf")); +} + +// Normal usage. +TEST(FieldTrialListTest, ParsesStructList) { + FieldTrialStructList my_list( + {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }), + FieldTrialStructMember("price", [](Garment* g) { return &g->price; })}, + {{1, "blue"}, {2, "red"}}); + + ParseFieldTrial({&my_list}, + "color:mauve|red|gold," + "price:10|20|30," + "other_param:asdf"); + + ASSERT_THAT(my_list.Get(), + ElementsAre(Garment{10, "mauve"}, Garment{20, "red"}, + Garment{30, "gold"})); +} + +// One FieldTrialList has the wrong length, so we use the user-provided default +// list. +TEST(FieldTrialListTest, StructListKeepsDefaultWithMismatchingLength) { + FieldTrialStructList my_list( + {FieldTrialStructMember("wrong_length", + [](Garment* g) { return &g->color; }), + FieldTrialStructMember("price", [](Garment* g) { return &g->price; })}, + {{1, "blue"}, {2, "red"}}); + + ParseFieldTrial({&my_list}, + "wrong_length:mauve|magenta|chartreuse|indigo," + "garment:hat|hat|crown," + "price:10|20|30"); + + ASSERT_THAT(my_list.Get(), + ElementsAre(Garment{1, "blue"}, Garment{2, "red"})); +} + +// One list is missing. We set the values we're given, and the others remain +// as whatever the Garment default constructor set them to. +TEST(FieldTrialListTest, StructListUsesDefaultForMissingList) { + FieldTrialStructList my_list( + {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }), + FieldTrialStructMember("price", [](Garment* g) { return &g->price; })}, + {{1, "blue"}, {2, "red"}}); + + ParseFieldTrial({&my_list}, "price:10|20|30"); + + ASSERT_THAT(my_list.Get(), + ElementsAre(Garment{10, ""}, Garment{20, ""}, Garment{30, ""})); +} + +// The user haven't provided values for any lists, so we use the default list. +TEST(FieldTrialListTest, StructListUsesDefaultListWithoutValues) { + FieldTrialStructList my_list( + {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }), + FieldTrialStructMember("price", [](Garment* g) { return &g->price; })}, + {{1, "blue"}, {2, "red"}}); + + ParseFieldTrial({&my_list}, ""); + + ASSERT_THAT(my_list.Get(), + ElementsAre(Garment{1, "blue"}, Garment{2, "red"})); +} + +// Some lists are provided and all are empty, so we return a empty list. +TEST(FieldTrialListTest, StructListHandlesEmptyLists) { + FieldTrialStructList my_list( + {FieldTrialStructMember("color", [](Garment* g) { return &g->color; }), + FieldTrialStructMember("price", [](Garment* g) { return &g->price; })}, + {{1, "blue"}, {2, "red"}}); + + ParseFieldTrial({&my_list}, "color,price"); + + ASSERT_EQ(my_list.Get().size(), 0u); +} + +} // namespace webrtc diff --git a/rtc_base/experiments/field_trial_parser.cc b/rtc_base/experiments/field_trial_parser.cc index f823b48e53..b2c48c94ac 100644 --- a/rtc_base/experiments/field_trial_parser.cc +++ b/rtc_base/experiments/field_trial_parser.cc @@ -1,5 +1,5 @@ /* - * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * Copyright 2019 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 @@ -33,9 +33,6 @@ FieldTrialParameterInterface::~FieldTrialParameterInterface() { RTC_DCHECK(used_) << "Field trial parameter with key: '" << key_ << "' never used."; } -std::string FieldTrialParameterInterface::Key() const { - return key_; -} void ParseFieldTrial( std::initializer_list fields, @@ -44,13 +41,23 @@ void ParseFieldTrial( FieldTrialParameterInterface* keyless_field = nullptr; for (FieldTrialParameterInterface* field : fields) { field->MarkAsUsed(); - if (field->Key().empty()) { + if (!field->sub_parameters_.empty()) { + for (FieldTrialParameterInterface* sub_field : field->sub_parameters_) { + RTC_DCHECK(!sub_field->key_.empty()); + sub_field->MarkAsUsed(); + field_map[sub_field->key_] = sub_field; + } + continue; + } + + if (field->key_.empty()) { RTC_DCHECK(!keyless_field); keyless_field = field; } else { - field_map[field->Key()] = field; + field_map[field->key_] = field; } } + size_t i = 0; while (i < trial_string.length()) { int val_end = FindOrEnd(trial_string, i, ','); @@ -78,6 +85,10 @@ void ParseFieldTrial( << "' (found in trial: \"" << trial_string << "\")"; } } + + for (FieldTrialParameterInterface* field : fields) { + field->ParseDone(); + } } template <> diff --git a/rtc_base/experiments/field_trial_parser.h b/rtc_base/experiments/field_trial_parser.h index e6ae4c52a6..a63cb20ef0 100644 --- a/rtc_base/experiments/field_trial_parser.h +++ b/rtc_base/experiments/field_trial_parser.h @@ -11,10 +11,13 @@ #define RTC_BASE_EXPERIMENTS_FIELD_TRIAL_PARSER_H_ #include + #include #include #include #include +#include + #include "absl/types/optional.h" // Field trial parser functionality. Provides funcitonality to parse field trial @@ -24,7 +27,7 @@ // 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 :. +// ,. If the key is provided without : a FieldTrialOptional will use nullopt. // Example string: "my_optional,my_int:3,my_string:hello" @@ -47,10 +50,14 @@ class FieldTrialParameterInterface { std::string raw_string); void MarkAsUsed() { used_ = true; } virtual bool Parse(absl::optional str_value) = 0; - std::string Key() const; + + virtual void ParseDone() {} + + std::vector sub_parameters_; + + std::string key_; private: - std::string key_; bool used_ = false; }; diff --git a/rtc_base/experiments/field_trial_parser_unittest.cc b/rtc_base/experiments/field_trial_parser_unittest.cc index 806b895e5f..cf483d7334 100644 --- a/rtc_base/experiments/field_trial_parser_unittest.cc +++ b/rtc_base/experiments/field_trial_parser_unittest.cc @@ -8,9 +8,12 @@ * be found in the AUTHORS file in the root of the source tree. */ #include "rtc_base/experiments/field_trial_parser.h" + +#include "rtc_base/experiments/field_trial_list.h" #include "rtc_base/gunit.h" #include "system_wrappers/include/field_trial.h" #include "test/field_trial.h" +#include "test/gmock.h" namespace webrtc { namespace { @@ -38,6 +41,7 @@ enum class CustomEnum { kRed = 1, kBlue = 2, }; + } // namespace TEST(FieldTrialParserTest, ParsesValidParameters) { @@ -152,4 +156,5 @@ TEST(FieldTrialParserTest, ParsesCustomEnumParameter) { ParseFieldTrial({&my_enum}, "e:5"); EXPECT_EQ(my_enum.Get(), CustomEnum::kBlue); } + } // namespace webrtc