loadtest_template.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. # This script generates a load test configuration template from a collection of
  16. # load test configurations.
  17. #
  18. # Configuration templates contain client and server configurations for multiple
  19. # languages, and may contain template substitution keys. These templates are
  20. # used to generate load test configurations by selecting clients and servers for
  21. # the required languages. The source files for template generation may be load
  22. # test configurations or load test configuration templates. Load test
  23. # configuration generation is performed by loadtest_config.py. See documentation
  24. # below:
  25. # https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md
  26. import argparse
  27. import os
  28. import sys
  29. from typing import Any, Dict, Iterable, List, Mapping, Type
  30. import yaml
  31. sys.path.append(os.path.dirname(os.path.abspath(__file__)))
  32. import loadtest_config
  33. TEMPLATE_FILE_HEADER_COMMENT = """
  34. # Template generated from load test configurations by loadtest_template.py.
  35. #
  36. # Configuration templates contain client and server configurations for multiple
  37. # languages, and may contain template substitution keys. These templates are
  38. # used to generate load test configurations by selecting clients and servers for
  39. # the required languages. The source files for template generation may be load
  40. # test configurations or load test configuration templates. Load test
  41. # configuration generation is performed by loadtest_config.py. See documentation
  42. # below:
  43. # https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md
  44. """
  45. def insert_worker(worker: Dict[str, Any], workers: List[Dict[str,
  46. Any]]) -> None:
  47. """Inserts client or server into a list, without inserting duplicates."""
  48. def dump(w):
  49. return yaml.dump(w, Dumper=yaml.SafeDumper, default_flow_style=False)
  50. worker_str = dump(worker)
  51. if any((worker_str == dump(w) for w in workers)):
  52. return
  53. workers.append(worker)
  54. def uniquify_workers(workermap: Dict[str, List[Dict[str, Any]]]) -> None:
  55. """Name workers if there is more than one for the same map key."""
  56. for workers in list(workermap.values()):
  57. if len(workers) <= 1:
  58. continue
  59. for i, worker in enumerate(workers):
  60. worker['name'] = str(i)
  61. def loadtest_template(
  62. input_file_names: Iterable[str],
  63. metadata: Mapping[str, Any],
  64. inject_client_pool: bool,
  65. inject_driver_image: bool,
  66. inject_driver_pool: bool,
  67. inject_server_pool: bool,
  68. inject_big_query_table: bool,
  69. inject_timeout_seconds: bool,
  70. inject_ttl_seconds: bool) -> Dict[str, Any]: # yapf: disable
  71. """Generates the load test template."""
  72. spec = dict() # type: Dict[str, Any]
  73. clientmap = dict() # Dict[str, List[Dict[str, Any]]]
  74. servermap = dict() # Dict[Str, List[Dict[str, Any]]]
  75. template = {
  76. 'apiVersion': 'e2etest.grpc.io/v1',
  77. 'kind': 'LoadTest',
  78. 'metadata': metadata,
  79. }
  80. for input_file_name in input_file_names:
  81. with open(input_file_name) as f:
  82. input_config = yaml.safe_load(f.read())
  83. if input_config.get('apiVersion') != template['apiVersion']:
  84. raise ValueError('Unexpected api version in file {}: {}'.format(
  85. input_file_name, input_config.get('apiVersion')))
  86. if input_config.get('kind') != template['kind']:
  87. raise ValueError('Unexpected kind in file {}: {}'.format(
  88. input_file_name, input_config.get('kind')))
  89. for client in input_config['spec']['clients']:
  90. del client['name']
  91. if inject_client_pool:
  92. client['pool'] = '${client_pool}'
  93. if client['language'] not in clientmap:
  94. clientmap[client['language']] = []
  95. insert_worker(client, clientmap[client['language']])
  96. for server in input_config['spec']['servers']:
  97. del server['name']
  98. if inject_server_pool:
  99. server['pool'] = '${server_pool}'
  100. if server['language'] not in servermap:
  101. servermap[server['language']] = []
  102. insert_worker(server, servermap[server['language']])
  103. input_spec = input_config['spec']
  104. del input_spec['clients']
  105. del input_spec['servers']
  106. del input_spec['scenariosJSON']
  107. spec.update(input_config['spec'])
  108. uniquify_workers(clientmap)
  109. uniquify_workers(servermap)
  110. spec.update({
  111. 'clients':
  112. sum((clientmap[language] for language in sorted(clientmap)),
  113. start=[]),
  114. 'servers':
  115. sum((servermap[language] for language in sorted(servermap)),
  116. start=[]),
  117. })
  118. if 'driver' not in spec:
  119. spec['driver'] = {'language': 'cxx'}
  120. driver = spec['driver']
  121. if 'name' in driver:
  122. del driver['name']
  123. if inject_driver_image:
  124. if 'run' not in driver:
  125. driver['run'] = [{'name': 'main'}]
  126. driver['run'][0]['image'] = '${driver_image}'
  127. if inject_driver_pool:
  128. driver['pool'] = '${driver_pool}'
  129. if 'run' not in driver:
  130. if inject_driver_pool:
  131. raise ValueError('Cannot inject driver.pool: missing driver.run.')
  132. del spec['driver']
  133. if inject_big_query_table:
  134. if 'results' not in spec:
  135. spec['results'] = dict()
  136. spec['results']['bigQueryTable'] = '${big_query_table}'
  137. if inject_timeout_seconds:
  138. spec['timeoutSeconds'] = '${timeout_seconds}'
  139. if inject_ttl_seconds:
  140. spec['ttlSeconds'] = '${ttl_seconds}'
  141. template['spec'] = spec
  142. return template
  143. def template_dumper(header_comment: str) -> Type[yaml.SafeDumper]:
  144. """Returns a custom dumper to dump templates in the expected format."""
  145. class TemplateDumper(yaml.SafeDumper):
  146. def expect_stream_start(self):
  147. super().expect_stream_start()
  148. if isinstance(self.event, yaml.StreamStartEvent):
  149. self.write_indent()
  150. self.write_indicator(header_comment, need_whitespace=False)
  151. def str_presenter(dumper, data):
  152. if '\n' in data:
  153. return dumper.represent_scalar('tag:yaml.org,2002:str',
  154. data,
  155. style='|')
  156. return dumper.represent_scalar('tag:yaml.org,2002:str', data)
  157. TemplateDumper.add_representer(str, str_presenter)
  158. return TemplateDumper
  159. def main() -> None:
  160. argp = argparse.ArgumentParser(
  161. description='Creates a load test config generator template.',
  162. fromfile_prefix_chars='@')
  163. argp.add_argument('-i',
  164. '--inputs',
  165. action='extend',
  166. nargs='+',
  167. type=str,
  168. help='Input files.')
  169. argp.add_argument('-o',
  170. '--output',
  171. type=str,
  172. help='Output file. Outputs to stdout if not set.')
  173. argp.add_argument(
  174. '--inject_client_pool',
  175. action='store_true',
  176. help='Set spec.client(s).pool values to \'${client_pool}\'.')
  177. argp.add_argument(
  178. '--inject_driver_image',
  179. action='store_true',
  180. help='Set spec.driver(s).image values to \'${driver_image}\'.')
  181. argp.add_argument(
  182. '--inject_driver_pool',
  183. action='store_true',
  184. help='Set spec.driver(s).pool values to \'${driver_pool}\'.')
  185. argp.add_argument(
  186. '--inject_server_pool',
  187. action='store_true',
  188. help='Set spec.server(s).pool values to \'${server_pool}\'.')
  189. argp.add_argument(
  190. '--inject_big_query_table',
  191. action='store_true',
  192. help='Set spec.results.bigQueryTable to \'${big_query_table}\'.')
  193. argp.add_argument('--inject_timeout_seconds',
  194. action='store_true',
  195. help='Set spec.timeoutSeconds to \'${timeout_seconds}\'.')
  196. argp.add_argument('--inject_ttl_seconds',
  197. action='store_true',
  198. help='Set timeout ')
  199. argp.add_argument('-n',
  200. '--name',
  201. default='',
  202. type=str,
  203. help='metadata.name.')
  204. argp.add_argument('-a',
  205. '--annotation',
  206. action='append',
  207. type=str,
  208. help='metadata.annotation(s), in the form key=value.',
  209. dest='annotations')
  210. args = argp.parse_args()
  211. annotations = loadtest_config.parse_key_value_args(args.annotations)
  212. metadata = {'name': args.name}
  213. if annotations:
  214. metadata['annotations'] = annotations
  215. template = loadtest_template(
  216. input_file_names=args.inputs,
  217. metadata=metadata,
  218. inject_client_pool=args.inject_client_pool,
  219. inject_driver_image=args.inject_driver_image,
  220. inject_driver_pool=args.inject_driver_pool,
  221. inject_server_pool=args.inject_server_pool,
  222. inject_big_query_table=args.inject_big_query_table,
  223. inject_timeout_seconds=args.inject_timeout_seconds,
  224. inject_ttl_seconds=args.inject_ttl_seconds)
  225. with open(args.output, 'w') if args.output else sys.stdout as f:
  226. yaml.dump(template,
  227. stream=f,
  228. Dumper=template_dumper(TEMPLATE_FILE_HEADER_COMMENT.strip()),
  229. default_flow_style=False)
  230. if __name__ == '__main__':
  231. main()