[experiments] Reduce experiment bloat on mobile platforms (#32253)

* see if experiments can lose weight

* test

* test

* test

* test

* contra test

* contra test

* add explainer

* Automated change: Fix sanity tests

* fixes

* fix

* strict-bs

* comments

* fixes

* iwyu

---------

Co-authored-by: ctiller <ctiller@users.noreply.github.com>
pull/32260/head
Craig Tiller 2 years ago committed by GitHub
parent 75f4ee6e5e
commit 9378a7d262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      BUILD
  2. 4
      src/core/BUILD
  3. 32
      src/core/lib/channel/connected_channel.cc
  4. 11
      src/core/lib/experiments/config.cc
  5. 10
      src/core/lib/experiments/config.h
  6. 2
      src/core/lib/experiments/experiments.cc
  7. 71
      src/core/lib/experiments/experiments.h
  8. 70
      src/core/lib/surface/call.cc
  9. 75
      tools/codegen/core/gen_experiments.py

18
BUILD

@ -54,6 +54,11 @@ config_setting(
values = {"define": "grpc_no_xds=true"},
)
config_setting(
name = "grpc_experiments_are_final_define",
values = {"define": "grpc_experiments_are_final=true"},
)
# When gRPC is build as shared library, binder transport code might still
# get included even when user's code does not depend on it. In that case
# --define=grpc_no_binder=true can be used to disable binder transport
@ -112,6 +117,19 @@ selects.config_setting_group(
],
)
selects.config_setting_group(
name = "grpc_experiments_are_final",
match_any = [
":grpc_experiments_are_final_define",
# In addition to disabling experiments when
# --define=grpc_experiments_are_final=true is specified, we also disable
# them on mobile platforms where runtime configuration of experiments is unlikely to be needed and where
# reducing the binary size is more important.
":android",
":ios",
],
)
# Fuzzers can be built as fuzzers or as tests
config_setting(
name = "grpc_build_fuzzers",

@ -108,6 +108,10 @@ grpc_cc_library(
"lib/experiments/config.h",
"lib/experiments/experiments.h",
],
defines = select({
"//:grpc_experiments_are_final": ["GRPC_EXPERIMENTS_ARE_FINAL"],
"//conditions:default": [],
}),
external_deps = ["absl/strings"],
language = "c++",
deps = [

@ -49,6 +49,7 @@
#include "src/core/lib/channel/channel_stack.h"
#include "src/core/lib/channel/context.h"
#include "src/core/lib/debug/trace.h"
#include "src/core/lib/experiments/experiments.h"
#include "src/core/lib/gpr/alloc.h"
#include "src/core/lib/gprpp/debug_location.h"
#include "src/core/lib/gprpp/match.h"
@ -248,6 +249,8 @@ static void connected_channel_get_channel_info(
namespace grpc_core {
namespace {
#if defined(GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_CLIENT_CALL) || \
defined(GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL)
class ConnectedChannelStream : public Orphanable {
public:
grpc_transport* transport() { return transport_; }
@ -662,7 +665,9 @@ class ConnectedChannelStream : public Orphanable {
grpc_transport_stream_op_batch_payload batch_payload_{
GetContext<grpc_call_context_element>()};
};
#endif
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_CLIENT_CALL
class ClientStream : public ConnectedChannelStream {
public:
ClientStream(grpc_transport* transport, CallArgs call_args)
@ -914,7 +919,9 @@ class ClientConnectedCallPromise {
private:
OrphanablePtr<ClientStream> impl_;
};
#endif
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL
class ServerStream final : public ConnectedChannelStream {
public:
ServerStream(grpc_transport* transport,
@ -1336,6 +1343,7 @@ class ServerConnectedCallPromise {
private:
OrphanablePtr<ServerStream> impl_;
};
#endif
template <ArenaPromise<ServerMetadataHandle> (*make_call_promise)(
grpc_transport*, CallArgs, NextPromiseFactory)>
@ -1346,15 +1354,15 @@ grpc_channel_filter MakeConnectedFilter() {
// In this way the filter can be inserted into either kind of channel stack,
// and only if all the filters in the stack are promise based will the call
// be promise based.
auto make_call_wrapper = +[](grpc_channel_element* elem, CallArgs call_args,
NextPromiseFactory next) {
grpc_transport* transport =
static_cast<channel_data*>(elem->channel_data)->transport;
return make_call_promise(transport, std::move(call_args), std::move(next));
};
return {
connected_channel_start_transport_stream_op_batch,
[](grpc_channel_element* elem, CallArgs call_args,
NextPromiseFactory next) {
grpc_transport* transport =
static_cast<channel_data*>(elem->channel_data)->transport;
return make_call_promise(transport, std::move(call_args),
std::move(next));
},
make_call_promise != nullptr ? make_call_wrapper : nullptr,
connected_channel_start_transport_op,
sizeof(call_data),
connected_channel_init_call_elem,
@ -1386,11 +1394,21 @@ ArenaPromise<ServerMetadataHandle> MakeTransportCallPromise(
const grpc_channel_filter kPromiseBasedTransportFilter =
MakeConnectedFilter<MakeTransportCallPromise>();
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_CLIENT_CALL
const grpc_channel_filter kClientEmulatedFilter =
MakeConnectedFilter<ClientConnectedCallPromise::Make>();
#else
const grpc_channel_filter kClientEmulatedFilter =
MakeConnectedFilter<nullptr>();
#endif
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL
const grpc_channel_filter kServerEmulatedFilter =
MakeConnectedFilter<ServerConnectedCallPromise::Make>();
#else
const grpc_channel_filter kServerEmulatedFilter =
MakeConnectedFilter<nullptr>();
#endif
} // namespace
} // namespace grpc_core

@ -30,10 +30,12 @@
#include <grpc/support/log.h>
#include "src/core/lib/experiments/experiments.h"
#include "src/core/lib/gprpp/crash.h" // IWYU pragma: keep
#include "src/core/lib/gprpp/global_config.h"
#include "src/core/lib/gprpp/memory.h"
#include "src/core/lib/gprpp/no_destruct.h"
#ifndef GRPC_EXPERIMENTS_ARE_FINAL
GPR_GLOBAL_CONFIG_DEFINE_STRING(
grpc_experiments, "",
"List of grpc experiments to enable (or with a '-' prefix to disable).");
@ -148,3 +150,12 @@ void ForceEnableExperiment(absl::string_view experiment, bool enable) {
}
} // namespace grpc_core
#else
namespace grpc_core {
void PrintExperimentsList() {}
void ForceEnableExperiment(absl::string_view experiment_name, bool) {
Crash(absl::StrCat("ForceEnableExperiment(\"", experiment_name,
"\") called in final build"));
}
} // namespace grpc_core
#endif

@ -21,12 +21,16 @@
#include "absl/strings/string_view.h"
// #define GRPC_EXPERIMENTS_ARE_FINAL
namespace grpc_core {
#ifndef GRPC_EXPERIMENTS_ARE_FINAL
// Return true if experiment \a experiment_id is enabled.
// Experiments are numbered by their order in the g_experiment_metadata array
// declared in experiments.h.
bool IsExperimentEnabled(size_t experiment_id);
#endif
// Print out a list of all experiments that are built into this binary.
void PrintExperimentsList();
@ -38,6 +42,12 @@ void PrintExperimentsList();
// If this is called twice for the same experiment, both calls must agree.
void ForceEnableExperiment(absl::string_view experiment_name, bool enable);
struct ExperimentMetadata {
const char* name;
const char* description;
bool default_value;
};
} // namespace grpc_core
#endif // GRPC_SRC_CORE_LIB_EXPERIMENTS_CONFIG_H

@ -18,6 +18,7 @@
#include "src/core/lib/experiments/experiments.h"
#ifndef GRPC_EXPERIMENTS_ARE_FINAL
namespace {
const char* const description_tcp_frame_size_tuning =
"If set, enables TCP to use RPC size estimation made by higher layers. TCP "
@ -81,3 +82,4 @@ const ExperimentMetadata g_experiment_metadata[] = {
};
} // namespace grpc_core
#endif

@ -13,6 +13,37 @@
// limitations under the License.
// Automatically generated by tools/codegen/core/gen_experiments.py
//
// This file contains the autogenerated parts of the experiments API.
//
// It generates two symbols for each experiment.
//
// For the experiment named new_car_project, it generates:
//
// - a function IsNewCarProjectEnabled() that returns true if the experiment
// should be enabled at runtime.
//
// - a macro GRPC_EXPERIMENT_IS_INCLUDED_NEW_CAR_PROJECT that is defined if the
// experiment *could* be enabled at runtime.
//
// The function is used to determine whether to run the experiment or
// non-experiment code path.
//
// If the experiment brings significant bloat, the macro can be used to avoid
// including the experiment code path in the binary for binaries that are size
// sensitive.
//
// By default that includes our iOS and Android builds.
//
// Finally, a small array is included that contains the metadata for each
// experiment.
//
// A macro, GRPC_EXPERIMENTS_ARE_FINAL, controls whether we fix experiment
// configuration at build time (if it's defined) or allow it to be tuned at
// runtime (if it's disabled).
//
// If you are using the Bazel build system, that macro can be configured with
// --define=grpc_experiments_are_final=true
#ifndef GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H
#define GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H
@ -25,38 +56,66 @@
namespace grpc_core {
#ifdef GRPC_EXPERIMENTS_ARE_FINAL
inline bool IsTcpFrameSizeTuningEnabled() { return false; }
inline bool IsTcpRcvLowatEnabled() { return false; }
inline bool IsPeerStateBasedFramingEnabled() { return false; }
#define GRPC_EXPERIMENT_IS_INCLUDED_FLOW_CONTROL_FIXES
inline bool IsFlowControlFixesEnabled() { return true; }
inline bool IsMemoryPressureControllerEnabled() { return false; }
inline bool IsUnconstrainedMaxQuotaBufferSizeEnabled() { return false; }
#define GRPC_EXPERIMENT_IS_INCLUDED_NEW_HPACK_HUFFMAN_DECODER
inline bool IsNewHpackHuffmanDecoderEnabled() { return true; }
inline bool IsEventEngineClientEnabled() { return false; }
#define GRPC_EXPERIMENT_IS_INCLUDED_MONITORING_EXPERIMENT
inline bool IsMonitoringExperimentEnabled() { return true; }
inline bool IsPromiseBasedClientCallEnabled() { return false; }
inline bool IsFreeLargeAllocatorEnabled() { return false; }
inline bool IsPromiseBasedServerCallEnabled() { return false; }
inline bool IsTransportSuppliesClientLatencyEnabled() { return false; }
inline bool IsEventEngineListenerEnabled() { return false; }
#else
#define GRPC_EXPERIMENT_IS_INCLUDED_TCP_FRAME_SIZE_TUNING
inline bool IsTcpFrameSizeTuningEnabled() { return IsExperimentEnabled(0); }
#define GRPC_EXPERIMENT_IS_INCLUDED_TCP_RCV_LOWAT
inline bool IsTcpRcvLowatEnabled() { return IsExperimentEnabled(1); }
#define GRPC_EXPERIMENT_IS_INCLUDED_PEER_STATE_BASED_FRAMING
inline bool IsPeerStateBasedFramingEnabled() { return IsExperimentEnabled(2); }
#define GRPC_EXPERIMENT_IS_INCLUDED_FLOW_CONTROL_FIXES
inline bool IsFlowControlFixesEnabled() { return IsExperimentEnabled(3); }
#define GRPC_EXPERIMENT_IS_INCLUDED_MEMORY_PRESSURE_CONTROLLER
inline bool IsMemoryPressureControllerEnabled() {
return IsExperimentEnabled(4);
}
#define GRPC_EXPERIMENT_IS_INCLUDED_UNCONSTRAINED_MAX_QUOTA_BUFFER_SIZE
inline bool IsUnconstrainedMaxQuotaBufferSizeEnabled() {
return IsExperimentEnabled(5);
}
#define GRPC_EXPERIMENT_IS_INCLUDED_NEW_HPACK_HUFFMAN_DECODER
inline bool IsNewHpackHuffmanDecoderEnabled() { return IsExperimentEnabled(6); }
#define GRPC_EXPERIMENT_IS_INCLUDED_EVENT_ENGINE_CLIENT
inline bool IsEventEngineClientEnabled() { return IsExperimentEnabled(7); }
#define GRPC_EXPERIMENT_IS_INCLUDED_MONITORING_EXPERIMENT
inline bool IsMonitoringExperimentEnabled() { return IsExperimentEnabled(8); }
#define GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_CLIENT_CALL
inline bool IsPromiseBasedClientCallEnabled() { return IsExperimentEnabled(9); }
#define GRPC_EXPERIMENT_IS_INCLUDED_FREE_LARGE_ALLOCATOR
inline bool IsFreeLargeAllocatorEnabled() { return IsExperimentEnabled(10); }
#define GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL
inline bool IsPromiseBasedServerCallEnabled() {
return IsExperimentEnabled(11);
}
#define GRPC_EXPERIMENT_IS_INCLUDED_TRANSPORT_SUPPLIES_CLIENT_LATENCY
inline bool IsTransportSuppliesClientLatencyEnabled() {
return IsExperimentEnabled(12);
}
#define GRPC_EXPERIMENT_IS_INCLUDED_EVENT_ENGINE_LISTENER
inline bool IsEventEngineListenerEnabled() { return IsExperimentEnabled(13); }
struct ExperimentMetadata {
const char* name;
const char* description;
bool default_value;
};
constexpr const size_t kNumExperiments = 14;
extern const ExperimentMetadata g_experiment_metadata[kNumExperiments];
#endif
} // namespace grpc_core
#endif // GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H

@ -2721,6 +2721,7 @@ void PublishMetadataArray(grpc_metadata_batch* md, grpc_metadata_array* array) {
///////////////////////////////////////////////////////////////////////////////
// ClientPromiseBasedCall
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_CLIENT_CALL
class ClientPromiseBasedCall final : public PromiseBasedCall {
public:
ClientPromiseBasedCall(Arena* arena, grpc_call_create_args* args)
@ -3105,10 +3106,13 @@ void ClientPromiseBasedCall::PublishStatus(
FinishOpOnCompletion(&recv_status_on_client_completion_,
PendingOp::kReceiveStatusOnClient);
}
#endif
///////////////////////////////////////////////////////////////////////////////
// ServerPromiseBasedCall
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL
class ServerPromiseBasedCall final : public PromiseBasedCall {
public:
ServerPromiseBasedCall(Arena* arena, grpc_call_create_args* args);
@ -3242,31 +3246,6 @@ class ServerPromiseBasedCall final : public PromiseBasedCall {
ClientMetadataHandle client_initial_metadata_ ABSL_GUARDED_BY(mu());
};
ArenaPromise<ServerMetadataHandle>
ServerCallContext::MakeTopOfServerCallPromise(
CallArgs call_args, grpc_completion_queue* cq,
grpc_metadata_array* publish_initial_metadata,
absl::FunctionRef<void(grpc_call* call)> publish) {
call_->mu()->AssertHeld();
call_->SetCompletionQueueLocked(cq);
call_->server_to_client_messages_ = call_args.server_to_client_messages;
call_->client_to_server_messages_ = call_args.client_to_server_messages;
call_->send_initial_metadata_state_ = call_args.server_initial_metadata;
call_->incoming_compression_algorithm_ =
call_args.client_initial_metadata->get(GrpcEncodingMetadata())
.value_or(GRPC_COMPRESS_NONE);
call_->client_initial_metadata_ =
std::move(call_args.client_initial_metadata);
PublishMetadataArray(call_->client_initial_metadata_.get(),
publish_initial_metadata);
call_->ExternalRef();
publish(call_->c_ptr());
return [this]() {
call_->mu()->AssertHeld();
return call_->PollTopOfCall();
};
}
ServerPromiseBasedCall::ServerPromiseBasedCall(Arena* arena,
grpc_call_create_args* args)
: PromiseBasedCall(arena, 0, *args),
@ -3506,6 +3485,43 @@ void ServerPromiseBasedCall::CancelWithErrorLocked(absl::Status error) {
ForceWakeup();
}
#endif
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL
ArenaPromise<ServerMetadataHandle>
ServerCallContext::MakeTopOfServerCallPromise(
CallArgs call_args, grpc_completion_queue* cq,
grpc_metadata_array* publish_initial_metadata,
absl::FunctionRef<void(grpc_call* call)> publish) {
call_->mu()->AssertHeld();
call_->SetCompletionQueueLocked(cq);
call_->server_to_client_messages_ = call_args.server_to_client_messages;
call_->client_to_server_messages_ = call_args.client_to_server_messages;
call_->send_initial_metadata_state_ = call_args.server_initial_metadata;
call_->incoming_compression_algorithm_ =
call_args.client_initial_metadata->get(GrpcEncodingMetadata())
.value_or(GRPC_COMPRESS_NONE);
call_->client_initial_metadata_ =
std::move(call_args.client_initial_metadata);
PublishMetadataArray(call_->client_initial_metadata_.get(),
publish_initial_metadata);
call_->ExternalRef();
publish(call_->c_ptr());
return [this]() {
call_->mu()->AssertHeld();
return call_->PollTopOfCall();
};
}
#else
ArenaPromise<ServerMetadataHandle>
ServerCallContext::MakeTopOfServerCallPromise(
CallArgs, grpc_completion_queue*, grpc_metadata_array*,
absl::FunctionRef<void(grpc_call*)>) {
(void)call_;
Crash("Promise-based server call is not enabled");
}
#endif
} // namespace grpc_core
///////////////////////////////////////////////////////////////////////////////
@ -3522,16 +3538,20 @@ size_t grpc_call_get_initial_size_estimate() {
grpc_error_handle grpc_call_create(grpc_call_create_args* args,
grpc_call** out_call) {
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_CLIENT_CALL
if (grpc_core::IsPromiseBasedClientCallEnabled() &&
args->server_transport_data == nullptr && args->channel->is_promising()) {
return grpc_core::MakePromiseBasedCall<grpc_core::ClientPromiseBasedCall>(
args, out_call);
}
#endif
#ifdef GRPC_EXPERIMENT_IS_INCLUDED_PROMISE_BASED_SERVER_CALL
if (grpc_core::IsPromiseBasedServerCallEnabled() &&
args->server_transport_data != nullptr && args->channel->is_promising()) {
return grpc_core::MakePromiseBasedCall<grpc_core::ServerPromiseBasedCall>(
args, out_call);
}
#endif
return grpc_core::FilterStackCall::Create(args, out_call);
}

@ -49,6 +49,22 @@ DEFAULTS = {
'release': 'kDefaultForReleaseOnly',
}
FINAL_RETURN = {
'broken': 'return false;',
False: 'return false;',
True: 'return true;',
'debug': '#ifdef NDEBUG\nreturn false;\n#else\nreturn true;\n#endif',
'release': '#ifdef NDEBUG\nreturn true;\n#else\nreturn false;\n#endif',
}
FINAL_DEFINE = {
'broken': None,
False: None,
True: '#define %s',
'debug': '#ifndef NDEBUG\n#define %s\n#endif',
'release': '#ifdef NDEBUG\n#define %s\n#endif',
}
BZL_LIST_FOR_DEFAULTS = {
'broken': None,
False: 'off',
@ -143,19 +159,46 @@ def put_copyright(file, prefix):
put_banner([file], [line[2:].rstrip() for line in copyright], prefix)
EXPERIMENT_METADATA = """struct ExperimentMetadata {
const char* name;
const char* description;
bool default_value;
};"""
WTF = """
This file contains the autogenerated parts of the experiments API.
It generates two symbols for each experiment.
For the experiment named new_car_project, it generates:
- a function IsNewCarProjectEnabled() that returns true if the experiment
should be enabled at runtime.
- a macro GRPC_EXPERIMENT_IS_INCLUDED_NEW_CAR_PROJECT that is defined if the
experiment *could* be enabled at runtime.
The function is used to determine whether to run the experiment or
non-experiment code path.
If the experiment brings significant bloat, the macro can be used to avoid
including the experiment code path in the binary for binaries that are size
sensitive.
By default that includes our iOS and Android builds.
Finally, a small array is included that contains the metadata for each
experiment.
A macro, GRPC_EXPERIMENTS_ARE_FINAL, controls whether we fix experiment
configuration at build time (if it's defined) or allow it to be tuned at
runtime (if it's disabled).
If you are using the Bazel build system, that macro can be configured with
--define=grpc_experiments_are_final=true
"""
with open('src/core/lib/experiments/experiments.h', 'w') as H:
put_copyright(H, "//")
put_banner(
[H],
["Automatically generated by tools/codegen/core/gen_experiments.py"],
"//")
["Automatically generated by tools/codegen/core/gen_experiments.py"] +
WTF.splitlines(), "//")
print("#ifndef GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H", file=H)
print("#define GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H", file=H)
@ -167,18 +210,30 @@ with open('src/core/lib/experiments/experiments.h', 'w') as H:
print(file=H)
print("namespace grpc_core {", file=H)
print(file=H)
print("#ifdef GRPC_EXPERIMENTS_ARE_FINAL", file=H)
for i, attr in enumerate(attrs):
define_fmt = FINAL_DEFINE[attr['default']]
if define_fmt:
print(define_fmt %
("GRPC_EXPERIMENT_IS_INCLUDED_%s" % attr['name'].upper()),
file=H)
print("inline bool Is%sEnabled() { %s }" %
(snake_to_pascal(attr['name']), FINAL_RETURN[attr['default']]),
file=H)
print("#else", file=H)
for i, attr in enumerate(attrs):
print("#define GRPC_EXPERIMENT_IS_INCLUDED_%s" % attr['name'].upper(),
file=H)
print("inline bool Is%sEnabled() { return IsExperimentEnabled(%d); }" %
(snake_to_pascal(attr['name']), i),
file=H)
print(file=H)
print(EXPERIMENT_METADATA, file=H)
print(file=H)
print("constexpr const size_t kNumExperiments = %d;" % len(attrs), file=H)
print(
"extern const ExperimentMetadata g_experiment_metadata[kNumExperiments];",
file=H)
print(file=H)
print("#endif", file=H)
print("} // namespace grpc_core", file=H)
print(file=H)
print("#endif // GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H", file=H)
@ -194,6 +249,7 @@ with open('src/core/lib/experiments/experiments.cc', 'w') as C:
print("#include <grpc/support/port_platform.h>", file=C)
print("#include \"src/core/lib/experiments/experiments.h\"", file=C)
print(file=C)
print("#ifndef GRPC_EXPERIMENTS_ARE_FINAL", file=C)
print("namespace {", file=C)
for attr in attrs:
print("const char* const description_%s = %s;" %
@ -224,6 +280,7 @@ with open('src/core/lib/experiments/experiments.cc', 'w') as C:
print("};", file=C)
print(file=C)
print("} // namespace grpc_core", file=C)
print("#endif", file=C)
bzl_to_tags_to_experiments = dict((key, collections.defaultdict(list))
for key in BZL_LIST_FOR_DEFAULTS.keys()

Loading…
Cancel
Save