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 <srte@webrtc.org>
Commit-Queue: Jonas Olsson <jonasolsson@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#27560}
diff --git a/rtc_base/experiments/BUILD.gn b/rtc_base/experiments/BUILD.gn
index b8709dc..70dcb5b 100644
--- a/rtc_base/experiments/BUILD.gn
+++ b/rtc_base/experiments/BUILD.gn
@@ -40,6 +40,8 @@
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 @@
"../../api/units:time_delta",
"../../rtc_base:checks",
"../../rtc_base:logging",
+ "../../rtc_base:stringutils",
"//third_party/abseil-cpp/absl/types:optional",
]
}
@@ -151,6 +154,7 @@
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 0000000..de897bf
--- /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<std::string> str_value) {
+ RTC_NOTREACHED();
+ return true;
+}
+
+int FieldTrialStructListBase::ValidateAndGetLength() {
+ int length = -1;
+ for (std::unique_ptr<FieldTrialListWrapper>& 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 0000000..cfbcce9
--- /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 <initializer_list>
+#include <memory>
+#include <string>
+#include <vector>
+
+#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<T> 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 <typename T>
+class FieldTrialList : public FieldTrialListBase {
+ public:
+ explicit FieldTrialList(std::string key) : FieldTrialList(key, {}) {}
+ FieldTrialList(std::string key, std::initializer_list<T> default_values)
+ : FieldTrialListBase(key), values_(default_values) {}
+
+ std::vector<T> Get() const { return values_; }
+ operator std::vector<T>() const { return Get(); }
+ const T& operator[](size_t index) const { return values_[index]; }
+ const std::vector<T>* operator->() const { return &values_; }
+
+ protected:
+ bool Parse(absl::optional<std::string> str_value) override {
+ parse_got_called_ = true;
+
+ if (!str_value) {
+ values_.clear();
+ return true;
+ }
+
+ std::vector<std::string> tokens;
+ std::vector<T> new_values_;
+ rtc::split(str_value.value(), '|', &tokens);
+
+ for (std::string token : tokens) {
+ absl::optional<T> value = ParseTypedParameter<T>(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<T> 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 <typename T>
+struct LambdaTypeTraits : public LambdaTypeTraits<decltype(&T::operator())> {};
+
+template <typename ClassType, typename RetType, typename SourceType>
+struct LambdaTypeTraits<RetType* (ClassType::*)(SourceType*)const> {
+ using ret = RetType;
+ using src = SourceType;
+};
+
+template <typename T>
+struct TypedFieldTrialListWrapper : FieldTrialListWrapper {
+ public:
+ TypedFieldTrialListWrapper(std::string key,
+ std::function<void(void*, T)> 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<T> list_;
+ std::function<void(void*, T)> sink_;
+};
+
+} // namespace field_trial_list_impl
+
+template <typename F,
+ typename Traits = typename field_trial_list_impl::LambdaTypeTraits<F>>
+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<typename Traits::src*>(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<FieldTrialListWrapper*> 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<FieldTrialListWrapper>(*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<std::string> str_value) override;
+
+ std::vector<std::unique_ptr<FieldTrialListWrapper>> sub_lists_;
+};
+
+template <typename S>
+class FieldTrialStructList : public FieldTrialStructListBase {
+ public:
+ FieldTrialStructList(std::initializer_list<FieldTrialListWrapper*> l,
+ std::initializer_list<S> default_list)
+ : FieldTrialStructListBase(l), values_(default_list) {}
+
+ std::vector<S> Get() const { return values_; }
+ operator std::vector<S>() const { return Get(); }
+ const S& operator[](size_t index) const { return values_[index]; }
+ const std::vector<S>* operator->() const { return &values_; }
+
+ protected:
+ void ParseDone() override {
+ int length = ValidateAndGetLength();
+
+ if (length == -1)
+ return;
+
+ std::vector<S> new_values(length, S());
+
+ for (std::unique_ptr<FieldTrialListWrapper>& 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<S> 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 0000000..a1abfe4
--- /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<int> 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<std::string> 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<Garment> 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<Garment> 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<Garment> 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<Garment> 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<Garment> 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 f823b48..b2c48c9 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 @@
RTC_DCHECK(used_) << "Field trial parameter with key: '" << key_
<< "' never used.";
}
-std::string FieldTrialParameterInterface::Key() const {
- return key_;
-}
void ParseFieldTrial(
std::initializer_list<FieldTrialParameterInterface*> fields,
@@ -44,13 +41,23 @@
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 @@
<< "' (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 e6ae4c5..a63cb20 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 <stdint.h>
+
#include <initializer_list>
#include <map>
#include <set>
#include <string>
+#include <vector>
+
#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 @@
std::string raw_string);
void MarkAsUsed() { used_ = true; }
virtual bool Parse(absl::optional<std::string> str_value) = 0;
- std::string Key() const;
+
+ virtual void ParseDone() {}
+
+ std::vector<FieldTrialParameterInterface*> 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 806b895..cf483d7 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 @@
kRed = 1,
kBlue = 2,
};
+
} // namespace
TEST(FieldTrialParserTest, ParsesValidParameters) {
@@ -152,4 +156,5 @@
ParseFieldTrial({&my_enum}, "e:5");
EXPECT_EQ(my_enum.Get(), CustomEnum::kBlue);
}
+
} // namespace webrtc