diff --git a/api/BUILD.gn b/api/BUILD.gn index c3f305126c..259b73c4d2 100644 --- a/api/BUILD.gn +++ b/api/BUILD.gn @@ -171,6 +171,7 @@ rtc_library("libjingle_peerconnection_api") { ":audio_options_api", ":callfactory_api", ":fec_controller_api", + ":field_trials", ":field_trials_view", ":frame_transformer_interface", ":libjingle_logging_api", @@ -1152,6 +1153,7 @@ if (rtc_include_tests) { sources = [ "array_view_unittest.cc", + "field_trials_unittest.cc", "function_view_unittest.cc", "rtc_error_unittest.cc", "rtc_event_log_output_file_unittest.cc", @@ -1166,6 +1168,8 @@ if (rtc_include_tests) { deps = [ ":array_view", ":create_time_controller", + ":field_trials", + ":field_trials_view", ":function_view", ":libjingle_peerconnection_api", ":rtc_error", @@ -1181,9 +1185,12 @@ if (rtc_include_tests) { "../rtc_base:rtc_task_queue", "../rtc_base:task_queue_for_test", "../rtc_base/task_utils:repeating_task", + "../system_wrappers:field_trial", "../test:fileutils", + "../test:rtc_expect_death", "../test:test_support", "task_queue:task_queue_default_factory_unittests", + "transport:field_trial_based_config", "units:time_delta", "units:timestamp", "units:units_unittests", @@ -1236,3 +1243,18 @@ rtc_source_set("webrtc_key_value_config") { sources = [ "webrtc_key_value_config.h" ] deps = [ ":field_trials_view" ] } + +rtc_library("field_trials") { + visibility = [ "*" ] + sources = [ + "field_trials.cc", + "field_trials.h", + ] + deps = [ + ":field_trials_view", + "../rtc_base:checks", + "../rtc_base/containers:flat_map", + "../system_wrappers:field_trial", + ] + absl_deps = [ "//third_party/abseil-cpp/absl/strings" ] +} diff --git a/api/DEPS b/api/DEPS index 06232d7890..9db91bef37 100644 --- a/api/DEPS +++ b/api/DEPS @@ -316,6 +316,10 @@ specific_include_rules = { "+modules/video_coding", ], + "field_trials\.h": [ + "+rtc_base/containers/flat_map.h", + ], + # .cc files in api/ should not be restricted in what they can #include, # so we re-add all the top-level directories here. (That's because .h # files leak their #includes to whoever's #including them, but .cc files diff --git a/api/field_trials.cc b/api/field_trials.cc new file mode 100644 index 0000000000..e6ca18b1cf --- /dev/null +++ b/api/field_trials.cc @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 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 "api/field_trials.h" + +#include + +#include "rtc_base/checks.h" +#include "system_wrappers/include/field_trial.h" + +namespace { + +// This part is copied from system_wrappers/field_trial.cc. +webrtc::flat_map InsertIntoMap(const std::string& s) { + std::string::size_type field_start = 0; + webrtc::flat_map key_value_map; + while (field_start < s.size()) { + std::string::size_type separator_pos = s.find('/', field_start); + RTC_CHECK_NE(separator_pos, std::string::npos) + << "Missing separator '/' after field trial key."; + RTC_CHECK_GT(separator_pos, field_start) + << "Field trial key cannot be empty."; + std::string key = s.substr(field_start, separator_pos - field_start); + field_start = separator_pos + 1; + + RTC_CHECK_LT(field_start, s.size()) + << "Missing value after field trial key. String ended."; + separator_pos = s.find('/', field_start); + RTC_CHECK_NE(separator_pos, std::string::npos) + << "Missing terminating '/' in field trial string."; + RTC_CHECK_GT(separator_pos, field_start) + << "Field trial value cannot be empty."; + std::string value = s.substr(field_start, separator_pos - field_start); + field_start = separator_pos + 1; + + // If a key is specified multiple times, only the value linked to the first + // key is stored. note: This will crash in debug build when calling + // InitFieldTrialsFromString(). + key_value_map.emplace(key, value); + } + // This check is technically redundant due to earlier checks. + // We nevertheless keep the check to make it clear that the entire + // string has been processed, and without indexing past the end. + RTC_CHECK_EQ(field_start, s.size()); + + return key_value_map; +} + +// Makes sure that only one instance is created, since the usage +// of global string makes behaviour unpredicatable otherwise. +// TODO(bugs.webrtc.org/10335): Remove once global string is gone. +std::atomic instance_created_{false}; + +} // namespace + +namespace webrtc { + +FieldTrials::FieldTrials(const std::string& s) + : field_trial_string_(s), + previous_field_trial_string_(webrtc::field_trial::GetFieldTrialString()), + key_value_map_(InsertIntoMap(s)) { + // TODO(bugs.webrtc.org/10335): Remove the global string! + field_trial::InitFieldTrialsFromString(field_trial_string_.c_str()); + RTC_CHECK(!instance_created_.exchange(true)) + << "Only one instance may be instanciated at any given time!"; +} + +FieldTrials::~FieldTrials() { + // TODO(bugs.webrtc.org/10335): Remove the global string! + field_trial::InitFieldTrialsFromString(previous_field_trial_string_); + RTC_CHECK(instance_created_.exchange(false)); +} + +std::string FieldTrials::Lookup(absl::string_view key) const { + auto it = key_value_map_.find(std::string(key)); + if (it != key_value_map_.end()) + return it->second; + + // Check the global string so that programs using + // a mix between FieldTrials and the global string continue to work + // TODO(bugs.webrtc.org/10335): Remove the global string! + return field_trial::FindFullName(std::string(key)); +} + +} // namespace webrtc diff --git a/api/field_trials.h b/api/field_trials.h new file mode 100644 index 0000000000..822e78c096 --- /dev/null +++ b/api/field_trials.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 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 API_FIELD_TRIALS_H_ +#define API_FIELD_TRIALS_H_ + +#include + +#include "absl/strings/string_view.h" +#include "api/field_trials_view.h" +#include "rtc_base/containers/flat_map.h" + +namespace webrtc { + +// The FieldTrials class is used to inject field trials into webrtc. +// +// Field trials allow webrtc clients (such as Chromium) to turn on feature code +// in binaries out in the field and gather information with that. +// +// They are designed to be easy to use with Chromium field trials and to speed +// up developers by reducing the need to wire up APIs to control whether a +// feature is on/off. +// +// The field trials are injected into objects that use them at creation time. +// +// NOTE: Creating multiple FieldTrials-object is currently prohibited +// until we remove the global string (TODO(bugs.webrtc.org/10335)) +class FieldTrials : public FieldTrialsView { + public: + explicit FieldTrials(const std::string& s); + ~FieldTrials(); + + std::string Lookup(absl::string_view key) const override; + + private: + const std::string field_trial_string_; + const char* const previous_field_trial_string_; + const flat_map key_value_map_; +}; + +} // namespace webrtc + +#endif // API_FIELD_TRIALS_H_ diff --git a/api/field_trials_unittest.cc b/api/field_trials_unittest.cc new file mode 100644 index 0000000000..fb2fff0db7 --- /dev/null +++ b/api/field_trials_unittest.cc @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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 "api/field_trials.h" + +#include "api/transport/field_trial_based_config.h" +#include "system_wrappers/include/field_trial.h" +#include "test/gtest.h" + +#if GTEST_HAS_DEATH_TEST && !defined(WEBRTC_ANDROID) +#include "test/testsupport/rtc_expect_death.h" +#endif // GTEST_HAS_DEATH_TEST && !defined(WEBRTC_ANDROID) + +namespace webrtc { + +TEST(FieldTrials, EmptyString) { + FieldTrials f(""); + EXPECT_FALSE(f.IsEnabled("MyCoolTrial")); + EXPECT_FALSE(f.IsDisabled("MyCoolTrial")); +} + +TEST(FieldTrials, EnableDisable) { + FieldTrials f("MyCoolTrial/Enabled/MyUncoolTrial/Disabled/"); + EXPECT_TRUE(f.IsEnabled("MyCoolTrial")); + EXPECT_TRUE(f.IsDisabled("MyUncoolTrial")); +} + +TEST(FieldTrials, SetGlobalStringAndReadFromFieldTrial) { + const char* s = "MyCoolTrial/Enabled/MyUncoolTrial/Disabled/"; + webrtc::field_trial::InitFieldTrialsFromString(s); + FieldTrialBasedConfig f; + EXPECT_TRUE(f.IsEnabled("MyCoolTrial")); + EXPECT_TRUE(f.IsDisabled("MyUncoolTrial")); +} + +TEST(FieldTrials, SetFieldTrialAndReadFromGlobalString) { + FieldTrials f("MyCoolTrial/Enabled/MyUncoolTrial/Disabled/"); + EXPECT_TRUE(webrtc::field_trial::IsEnabled("MyCoolTrial")); + EXPECT_TRUE(webrtc::field_trial::IsDisabled("MyUncoolTrial")); +} + +TEST(FieldTrials, RestoresGlobalString) { + const char* s = "SomeString/Enabled/"; + webrtc::field_trial::InitFieldTrialsFromString(s); + { + FieldTrials f("SomeOtherString/Enabled/"); + EXPECT_EQ(std::string("SomeOtherString/Enabled/"), + webrtc::field_trial::GetFieldTrialString()); + } + EXPECT_EQ(s, webrtc::field_trial::GetFieldTrialString()); +} + +#if GTEST_HAS_DEATH_TEST && !defined(WEBRTC_ANDROID) +TEST(FieldTrials, OnlyOneInstance) { + FieldTrials f("SomeString/Enabled/"); + RTC_EXPECT_DEATH(FieldTrials("SomeOtherString/Enabled/").Lookup("Whatever"), + "Only one instance"); +} +#endif // GTEST_HAS_DEATH_TEST && !defined(WEBRTC_ANDROID) + +TEST(FieldTrials, SequentialInstances) { + { FieldTrials f("SomeString/Enabled/"); } + { FieldTrials f("SomeOtherString/Enabled/"); } +} + +} // namespace webrtc