xref: /aosp_15_r20/external/grpc-grpc/tools/run_tests/performance/loadtest_template.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1#!/usr/bin/env python3
2# Copyright 2021 The gRPC Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16# This script generates a load test configuration template from a collection of
17# load test configurations.
18#
19# Configuration templates contain client and server configurations for multiple
20# languages, and may contain template substitution keys. These templates are
21# used to generate load test configurations by selecting clients and servers for
22# the required languages. The source files for template generation may be load
23# test configurations or load test configuration templates. Load test
24# configuration generation is performed by loadtest_config.py. See documentation
25# below:
26# https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md
27
28import argparse
29import os
30import sys
31from typing import Any, Dict, Iterable, List, Mapping, Type
32
33import yaml
34
35sys.path.append(os.path.dirname(os.path.abspath(__file__)))
36import loadtest_config
37
38TEMPLATE_FILE_HEADER_COMMENT = """
39# Template generated from load test configurations by loadtest_template.py.
40#
41# Configuration templates contain client and server configurations for multiple
42# languages, and may contain template substitution keys. These templates are
43# used to generate load test configurations by selecting clients and servers for
44# the required languages. The source files for template generation may be load
45# test configurations or load test configuration templates. Load test
46# configuration generation is performed by loadtest_config.py. See documentation
47# below:
48# https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md
49"""
50
51
52def insert_worker(
53    worker: Dict[str, Any], workers: List[Dict[str, Any]]
54) -> None:
55    """Inserts client or server into a list, without inserting duplicates."""
56
57    def dump(w):
58        return yaml.dump(w, Dumper=yaml.SafeDumper, default_flow_style=False)
59
60    worker_str = dump(worker)
61    if any((worker_str == dump(w) for w in workers)):
62        return
63    workers.append(worker)
64
65
66def uniquify_workers(workermap: Dict[str, List[Dict[str, Any]]]) -> None:
67    """Name workers if there is more than one for the same map key."""
68    for workers in list(workermap.values()):
69        if len(workers) <= 1:
70            continue
71        for i, worker in enumerate(workers):
72            worker["name"] = str(i)
73
74
75def loadtest_template(
76        input_file_names: Iterable[str],
77        metadata: Mapping[str, Any],
78        inject_client_pool: bool,
79        inject_driver_image: bool,
80        inject_driver_pool: bool,
81        inject_server_pool: bool,
82        inject_big_query_table: bool,
83        inject_timeout_seconds: bool,
84        inject_ttl_seconds: bool) -> Dict[str, Any]:  # fmt: skip
85    """Generates the load test template."""
86    spec = dict()  # type: Dict[str, Any]
87    clientmap = dict()  # Dict[str, List[Dict[str, Any]]]
88    servermap = dict()  # Dict[Str, List[Dict[str, Any]]]
89    template = {
90        "apiVersion": "e2etest.grpc.io/v1",
91        "kind": "LoadTest",
92        "metadata": metadata,
93    }
94    for input_file_name in input_file_names:
95        with open(input_file_name) as f:
96            input_config = yaml.safe_load(f.read())
97
98            if input_config.get("apiVersion") != template["apiVersion"]:
99                raise ValueError(
100                    "Unexpected api version in file {}: {}".format(
101                        input_file_name, input_config.get("apiVersion")
102                    )
103                )
104            if input_config.get("kind") != template["kind"]:
105                raise ValueError(
106                    "Unexpected kind in file {}: {}".format(
107                        input_file_name, input_config.get("kind")
108                    )
109                )
110
111            for client in input_config["spec"]["clients"]:
112                del client["name"]
113                if inject_client_pool:
114                    client["pool"] = "${client_pool}"
115                if client["language"] not in clientmap:
116                    clientmap[client["language"]] = []
117                insert_worker(client, clientmap[client["language"]])
118
119            for server in input_config["spec"]["servers"]:
120                del server["name"]
121                if inject_server_pool:
122                    server["pool"] = "${server_pool}"
123                if server["language"] not in servermap:
124                    servermap[server["language"]] = []
125                insert_worker(server, servermap[server["language"]])
126
127            input_spec = input_config["spec"]
128            del input_spec["clients"]
129            del input_spec["servers"]
130            del input_spec["scenariosJSON"]
131            spec.update(input_config["spec"])
132
133    uniquify_workers(clientmap)
134    uniquify_workers(servermap)
135
136    spec.update(
137        {
138            "clients": sum(
139                (clientmap[language] for language in sorted(clientmap)),
140                start=[],
141            ),
142            "servers": sum(
143                (servermap[language] for language in sorted(servermap)),
144                start=[],
145            ),
146        }
147    )
148
149    if "driver" not in spec:
150        spec["driver"] = {"language": "cxx"}
151
152    driver = spec["driver"]
153    if "name" in driver:
154        del driver["name"]
155    if inject_driver_image:
156        if "run" not in driver:
157            driver["run"] = [{"name": "main"}]
158        driver["run"][0]["image"] = "${driver_image}"
159    if inject_driver_pool:
160        driver["pool"] = "${driver_pool}"
161
162    if "run" not in driver:
163        if inject_driver_pool:
164            raise ValueError("Cannot inject driver.pool: missing driver.run.")
165        del spec["driver"]
166
167    if inject_big_query_table:
168        if "results" not in spec:
169            spec["results"] = dict()
170        spec["results"]["bigQueryTable"] = "${big_query_table}"
171    if inject_timeout_seconds:
172        spec["timeoutSeconds"] = "${timeout_seconds}"
173    if inject_ttl_seconds:
174        spec["ttlSeconds"] = "${ttl_seconds}"
175
176    template["spec"] = spec
177
178    return template
179
180
181def template_dumper(header_comment: str) -> Type[yaml.SafeDumper]:
182    """Returns a custom dumper to dump templates in the expected format."""
183
184    class TemplateDumper(yaml.SafeDumper):
185        def expect_stream_start(self):
186            super().expect_stream_start()
187            if isinstance(self.event, yaml.StreamStartEvent):
188                self.write_indent()
189                self.write_indicator(header_comment, need_whitespace=False)
190
191    def str_presenter(dumper, data):
192        if "\n" in data:
193            return dumper.represent_scalar(
194                "tag:yaml.org,2002:str", data, style="|"
195            )
196        return dumper.represent_scalar("tag:yaml.org,2002:str", data)
197
198    TemplateDumper.add_representer(str, str_presenter)
199
200    return TemplateDumper
201
202
203def main() -> None:
204    argp = argparse.ArgumentParser(
205        description="Creates a load test config generator template.",
206        fromfile_prefix_chars="@",
207    )
208    argp.add_argument(
209        "-i",
210        "--inputs",
211        action="extend",
212        nargs="+",
213        type=str,
214        help="Input files.",
215    )
216    argp.add_argument(
217        "-o",
218        "--output",
219        type=str,
220        help="Output file. Outputs to stdout if not set.",
221    )
222    argp.add_argument(
223        "--inject_client_pool",
224        action="store_true",
225        help="Set spec.client(s).pool values to '${client_pool}'.",
226    )
227    argp.add_argument(
228        "--inject_driver_image",
229        action="store_true",
230        help="Set spec.driver(s).image values to '${driver_image}'.",
231    )
232    argp.add_argument(
233        "--inject_driver_pool",
234        action="store_true",
235        help="Set spec.driver(s).pool values to '${driver_pool}'.",
236    )
237    argp.add_argument(
238        "--inject_server_pool",
239        action="store_true",
240        help="Set spec.server(s).pool values to '${server_pool}'.",
241    )
242    argp.add_argument(
243        "--inject_big_query_table",
244        action="store_true",
245        help="Set spec.results.bigQueryTable to '${big_query_table}'.",
246    )
247    argp.add_argument(
248        "--inject_timeout_seconds",
249        action="store_true",
250        help="Set spec.timeoutSeconds to '${timeout_seconds}'.",
251    )
252    argp.add_argument(
253        "--inject_ttl_seconds", action="store_true", help="Set timeout "
254    )
255    argp.add_argument(
256        "-n", "--name", default="", type=str, help="metadata.name."
257    )
258    argp.add_argument(
259        "-a",
260        "--annotation",
261        action="append",
262        type=str,
263        help="metadata.annotation(s), in the form key=value.",
264        dest="annotations",
265    )
266    args = argp.parse_args()
267
268    annotations = loadtest_config.parse_key_value_args(args.annotations)
269
270    metadata = {"name": args.name}
271    if annotations:
272        metadata["annotations"] = annotations
273
274    template = loadtest_template(
275        input_file_names=args.inputs,
276        metadata=metadata,
277        inject_client_pool=args.inject_client_pool,
278        inject_driver_image=args.inject_driver_image,
279        inject_driver_pool=args.inject_driver_pool,
280        inject_server_pool=args.inject_server_pool,
281        inject_big_query_table=args.inject_big_query_table,
282        inject_timeout_seconds=args.inject_timeout_seconds,
283        inject_ttl_seconds=args.inject_ttl_seconds,
284    )
285
286    with open(args.output, "w") if args.output else sys.stdout as f:
287        yaml.dump(
288            template,
289            stream=f,
290            Dumper=template_dumper(TEMPLATE_FILE_HEADER_COMMENT.strip()),
291            default_flow_style=False,
292        )
293
294
295if __name__ == "__main__":
296    main()
297