The C based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)
https://grpc.io/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
16 KiB
449 lines
16 KiB
#!/usr/bin/env python3 |
|
|
|
# Copyright 2023 gRPC authors. |
|
# |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
# you may not use this file except in compliance with the License. |
|
# You may obtain a copy of the License at |
|
# |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
# |
|
# Unless required by applicable law or agreed to in writing, software |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
# See the License for the specific language governing permissions and |
|
# limitations under the License. |
|
""" |
|
A module to assist in generating experiment related code and artifacts. |
|
""" |
|
|
|
from __future__ import print_function |
|
|
|
import collections |
|
import ctypes |
|
import datetime |
|
import json |
|
import math |
|
import os |
|
import re |
|
import sys |
|
|
|
import yaml |
|
|
|
_CODEGEN_PLACEHOLDER_TEXT = """ |
|
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 |
|
""" |
|
|
|
|
|
def ToCStr(s, encoding="ascii"): |
|
if isinstance(s, str): |
|
s = s.encode(encoding) |
|
result = "" |
|
for c in s: |
|
c = chr(c) if isinstance(c, int) else c |
|
if not (32 <= ord(c) < 127) or c in ("\\", '"'): |
|
result += "\\%03o" % ord(c) |
|
else: |
|
result += c |
|
return '"' + result + '"' |
|
|
|
|
|
def SnakeToPascal(s): |
|
return "".join(x.capitalize() for x in s.split("_")) |
|
|
|
|
|
def PutBanner(files, banner, prefix): |
|
# Print a big comment block into a set of files |
|
for f in files: |
|
for line in banner: |
|
if not line: |
|
print(prefix, file=f) |
|
else: |
|
print("%s %s" % (prefix, line), file=f) |
|
print(file=f) |
|
|
|
|
|
def PutCopyright(file, prefix): |
|
# copy-paste copyright notice from this file |
|
with open(__file__) as my_source: |
|
copyright = [] |
|
for line in my_source: |
|
if line[0] != "#": |
|
break |
|
for line in my_source: |
|
if line[0] == "#": |
|
copyright.append(line) |
|
break |
|
for line in my_source: |
|
if line[0] != "#": |
|
break |
|
copyright.append(line) |
|
PutBanner([file], [line[2:].rstrip() for line in copyright], prefix) |
|
|
|
|
|
class ExperimentDefinition(object): |
|
def __init__(self, attributes): |
|
self._error = False |
|
if "name" not in attributes: |
|
print("ERROR: experiment with no name: %r" % attributes) |
|
self._error = True |
|
if "description" not in attributes: |
|
print( |
|
"ERROR: no description for experiment %s" % attributes["name"] |
|
) |
|
self._error = True |
|
if "owner" not in attributes: |
|
print("ERROR: no owner for experiment %s" % attributes["name"]) |
|
self._error = True |
|
if "expiry" not in attributes: |
|
print("ERROR: no expiry for experiment %s" % attributes["name"]) |
|
self._error = True |
|
if attributes["name"] == "monitoring_experiment": |
|
if attributes["expiry"] != "never-ever": |
|
print("ERROR: monitoring_experiment should never expire") |
|
self._error = True |
|
if self._error: |
|
print("Failed to create experiment definition") |
|
return |
|
self._allow_in_fuzzing_config = True |
|
self._name = attributes["name"] |
|
self._description = attributes["description"] |
|
self._expiry = attributes["expiry"] |
|
self._default = None |
|
self._additional_constraints = {} |
|
self._test_tags = [] |
|
|
|
if "allow_in_fuzzing_config" in attributes: |
|
self._allow_in_fuzzing_config = attributes[ |
|
"allow_in_fuzzing_config" |
|
] |
|
|
|
if "test_tags" in attributes: |
|
self._test_tags = attributes["test_tags"] |
|
|
|
def IsValid(self, check_expiry=False): |
|
if self._error: |
|
return False |
|
if not check_expiry: |
|
return True |
|
if ( |
|
self._name == "monitoring_experiment" |
|
and self._expiry == "never-ever" |
|
): |
|
return True |
|
today = datetime.date.today() |
|
two_quarters_from_now = today + datetime.timedelta(days=180) |
|
expiry = datetime.datetime.strptime(self._expiry, "%Y/%m/%d").date() |
|
if expiry < today: |
|
print( |
|
"WARNING: experiment %s expired on %s" |
|
% (self._name, self._expiry) |
|
) |
|
if expiry > two_quarters_from_now: |
|
print( |
|
"WARNING: experiment %s expires far in the future on %s" |
|
% (self._name, self._expiry) |
|
) |
|
print("expiry should be no more than two quarters from now") |
|
return not self._error |
|
|
|
def AddRolloutSpecification(self, allowed_defaults, rollout_attributes): |
|
if self._error or self._default is not None: |
|
return False |
|
if rollout_attributes["name"] != self._name: |
|
print( |
|
"ERROR: Rollout specification does not apply to this" |
|
" experiment: %s" % self._name |
|
) |
|
return False |
|
if "default" not in rollout_attributes: |
|
print( |
|
"ERROR: no default for experiment %s" |
|
% rollout_attributes["name"] |
|
) |
|
self._error = True |
|
if rollout_attributes["default"] not in allowed_defaults: |
|
print( |
|
"ERROR: invalid default for experiment %s: %r" |
|
% (rollout_attributes["name"], rollout_attributes["default"]) |
|
) |
|
self._error = True |
|
if "additional_constraints" in rollout_attributes: |
|
self._additional_constraints = rollout_attributes[ |
|
"additional_constraints" |
|
] |
|
self._default = rollout_attributes["default"] |
|
return True |
|
|
|
@property |
|
def name(self): |
|
return self._name |
|
|
|
@property |
|
def description(self): |
|
return self._description |
|
|
|
@property |
|
def default(self): |
|
return self._default |
|
|
|
@property |
|
def test_tags(self): |
|
return self._test_tags |
|
|
|
@property |
|
def allow_in_fuzzing_config(self): |
|
return self._allow_in_fuzzing_config |
|
|
|
@property |
|
def additional_constraints(self): |
|
return self._additional_constraints |
|
|
|
|
|
class ExperimentsCompiler(object): |
|
def __init__( |
|
self, defaults, final_return, final_define, bzl_list_for_defaults=None |
|
): |
|
self._defaults = defaults |
|
self._final_return = final_return |
|
self._final_define = final_define |
|
self._bzl_list_for_defaults = bzl_list_for_defaults |
|
self._experiment_definitions = {} |
|
self._experiment_rollouts = {} |
|
|
|
def AddExperimentDefinition(self, experiment_definition): |
|
if experiment_definition.name in self._experiment_definitions: |
|
print( |
|
"ERROR: Duplicate experiment definition: %s" |
|
% experiment_definition.name |
|
) |
|
return False |
|
self._experiment_definitions[ |
|
experiment_definition.name |
|
] = experiment_definition |
|
return True |
|
|
|
def AddRolloutSpecification(self, rollout_attributes): |
|
if "name" not in rollout_attributes: |
|
print( |
|
"ERROR: experiment with no name: %r in rollout_attribute" |
|
% rollout_attributes |
|
) |
|
return False |
|
if rollout_attributes["name"] not in self._experiment_definitions: |
|
print( |
|
"WARNING: rollout for an undefined experiment: %s ignored" |
|
% rollout_attributes["name"] |
|
) |
|
return self._experiment_definitions[ |
|
rollout_attributes["name"] |
|
].AddRolloutSpecification(self._defaults, rollout_attributes) |
|
|
|
def GenerateExperimentsHdr(self, output_file): |
|
with open(output_file, "w") as H: |
|
PutCopyright(H, "//") |
|
PutBanner( |
|
[H], |
|
["Auto generated by tools/codegen/core/gen_experiments.py"] |
|
+ _CODEGEN_PLACEHOLDER_TEXT.splitlines(), |
|
"//", |
|
) |
|
|
|
print("#ifndef GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H", file=H) |
|
print("#define GRPC_SRC_CORE_LIB_EXPERIMENTS_EXPERIMENTS_H", file=H) |
|
print(file=H) |
|
print("#include <grpc/support/port_platform.h>", file=H) |
|
print(file=H) |
|
print("#include <stddef.h>", file=H) |
|
print('#include "src/core/lib/experiments/config.h"', file=H) |
|
print(file=H) |
|
print("namespace grpc_core {", file=H) |
|
print(file=H) |
|
print("#ifdef GRPC_EXPERIMENTS_ARE_FINAL", file=H) |
|
for _, exp in self._experiment_definitions.items(): |
|
define_fmt = self._final_define[exp.default] |
|
if define_fmt: |
|
print( |
|
define_fmt |
|
% ("GRPC_EXPERIMENT_IS_INCLUDED_%s" % exp.name.upper()), |
|
file=H, |
|
) |
|
print( |
|
"inline bool Is%sEnabled() { %s }" |
|
% ( |
|
SnakeToPascal(exp.name), |
|
self._final_return[exp.default], |
|
), |
|
file=H, |
|
) |
|
print("#else", file=H) |
|
for i, (_, exp) in enumerate(self._experiment_definitions.items()): |
|
print( |
|
"#define GRPC_EXPERIMENT_IS_INCLUDED_%s" % exp.name.upper(), |
|
file=H, |
|
) |
|
print( |
|
"inline bool Is%sEnabled() { return" |
|
" IsExperimentEnabled(%d); }" |
|
% (SnakeToPascal(exp.name), i), |
|
file=H, |
|
) |
|
print(file=H) |
|
print( |
|
"constexpr const size_t kNumExperiments = %d;" |
|
% len(self._experiment_definitions.keys()), |
|
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 |
|
) |
|
|
|
def GenerateExperimentsSrc(self, output_file): |
|
with open(output_file, "w") as C: |
|
PutCopyright(C, "//") |
|
PutBanner( |
|
[C], |
|
["Auto generated by tools/codegen/core/gen_experiments.py"], |
|
"//", |
|
) |
|
|
|
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) |
|
have_defaults = set() |
|
for _, exp in self._experiment_definitions.items(): |
|
print( |
|
"const char* const description_%s = %s;" |
|
% (exp.name, ToCStr(exp.description)), |
|
file=C, |
|
) |
|
print( |
|
"const char* const additional_constraints_%s = %s;" |
|
% ( |
|
exp.name, |
|
ToCStr(json.dumps(exp.additional_constraints)), |
|
), |
|
file=C, |
|
) |
|
have_defaults.add(self._defaults[exp.default]) |
|
if "kDefaultForDebugOnly" in have_defaults: |
|
print("#ifdef NDEBUG", file=C) |
|
if "kDefaultForDebugOnly" in have_defaults: |
|
print("const bool kDefaultForDebugOnly = false;", file=C) |
|
print("#else", file=C) |
|
if "kDefaultForDebugOnly" in have_defaults: |
|
print("const bool kDefaultForDebugOnly = true;", file=C) |
|
print("#endif", file=C) |
|
print("}", file=C) |
|
print(file=C) |
|
print("namespace grpc_core {", file=C) |
|
print(file=C) |
|
print( |
|
"const ExperimentMetadata g_experiment_metadata[] = {", file=C |
|
) |
|
for _, exp in self._experiment_definitions.items(): |
|
print( |
|
" {%s, description_%s, additional_constraints_%s, %s, %s}," |
|
% ( |
|
ToCStr(exp.name), |
|
exp.name, |
|
exp.name, |
|
self._defaults[exp.default], |
|
"true" if exp.allow_in_fuzzing_config else "false", |
|
), |
|
file=C, |
|
) |
|
print("};", file=C) |
|
print(file=C) |
|
print("} // namespace grpc_core", file=C) |
|
print("#endif", file=C) |
|
|
|
def GenExperimentsBzl(self, output_file): |
|
if self._bzl_list_for_defaults is None: |
|
return |
|
|
|
bzl_to_tags_to_experiments = dict( |
|
(key, collections.defaultdict(list)) |
|
for key in self._bzl_list_for_defaults.keys() |
|
if key is not None |
|
) |
|
|
|
for _, exp in self._experiment_definitions.items(): |
|
for tag in exp.test_tags: |
|
bzl_to_tags_to_experiments[exp.default][tag].append(exp.name) |
|
|
|
with open(output_file, "w") as B: |
|
PutCopyright(B, "#") |
|
PutBanner( |
|
[B], |
|
["Auto generated by tools/codegen/core/gen_experiments.py"], |
|
"#", |
|
) |
|
|
|
print( |
|
( |
|
'"""Dictionary of tags to experiments so we know when to' |
|
' test different experiments."""' |
|
), |
|
file=B, |
|
) |
|
|
|
bzl_to_tags_to_experiments = sorted( |
|
(self._bzl_list_for_defaults[default], tags_to_experiments) |
|
for default, tags_to_experiments in bzl_to_tags_to_experiments.items() |
|
if self._bzl_list_for_defaults[default] is not None |
|
) |
|
|
|
print(file=B) |
|
print("EXPERIMENTS = {", file=B) |
|
for key, tags_to_experiments in bzl_to_tags_to_experiments: |
|
print(' "%s": {' % key, file=B) |
|
for tag, experiments in sorted(tags_to_experiments.items()): |
|
print(' "%s": [' % tag, file=B) |
|
for experiment in sorted(experiments): |
|
print(' "%s",' % experiment, file=B) |
|
print(" ],", file=B) |
|
print(" },", file=B) |
|
print("}", file=B)
|
|
|