this repo has no description
at trunk 388 lines 14 kB view raw
1#!/usr/bin/env python3 2import argparse 3import json 4import logging 5import os 6import shlex 7import subprocess 8import sys 9from _display_results import build_table 10from _tools import add_tools_arguments, PARALLEL_TOOLS, SEQUENTIAL_TOOLS 11from typing import Optional 12 13 14log = logging.getLogger(__name__) 15 16 17class Benchmark: 18 def __init__(self, path, name, ext): 19 self.name = name 20 self.path = path 21 assert ext == ".py" 22 self.ext = ext 23 self.bytecode = {} # mapping of Interpreter to bytecode location 24 25 def __lt__(self, other): 26 return self.name.__lt__(other.name) 27 28 def __ne__(self, other): 29 return not self.__eq__(other) 30 31 def __eq__(self, other): 32 return type(other) is Benchmark and self.name.__eq__(other.name) 33 34 def __repr__(self): 35 return self.name 36 37 def filepath(self): 38 return f"{self.path}/{self.name}{self.ext}" 39 40 41class BenchmarkRunner: 42 def __init__(self, args, interpreters): 43 self.interpreters = interpreters 44 self.path = sys.argv[0].rsplit("/", 1)[0] 45 self._register_measurement_tools(args["tools"], args) 46 47 def _register_measurement_tools(self, tool_list, args): 48 sys.path.append(self.path) 49 sys.path.pop() 50 self.tools = [] 51 for tool in SEQUENTIAL_TOOLS: 52 if tool.NAME in tool_list: 53 self.tools.append(tool(args)) 54 self.parallel_tools = [] 55 for tool in PARALLEL_TOOLS: 56 if tool.NAME in tool_list: 57 self.parallel_tools.append(tool(args)) 58 59 @staticmethod 60 def merge_parallel_results(results, parallel_results): 61 results = sorted(results, key=lambda x: (x["benchmark"], x["interpreter"])) 62 parallel_results = sorted( 63 parallel_results, key=lambda x: (x["benchmark"], x["interpreter"]) 64 ) 65 for seq_result, parallel_result in zip(results, parallel_results): 66 seq_result.update(parallel_result) 67 return results 68 69 def run_benchmarks(self): 70 results = [] 71 for interpreter in self.interpreters: 72 log.info(f"Running interpreter: {interpreter.name}") 73 for benchmark in interpreter.benchmarks_to_run: 74 log.info(f"Running benchmark: {benchmark.name}") 75 result = {"benchmark": benchmark.name, "interpreter": interpreter.name} 76 for tool in self.tools: 77 tool_result = tool.execute(interpreter, benchmark) 78 result.update(tool_result) 79 results.append(result) 80 for tool in self.parallel_tools: 81 parallel_results = tool.execute_parallel( 82 self.interpreters, self.interpreters[0].benchmarks_to_run 83 ) 84 results = self.merge_parallel_results(results, parallel_results) 85 return results 86 87 88class Interpreter: 89 CPYTHON = "fbcode-python" 90 CPYTHON_PATH = "/usr/bin/python3.8" 91 92 def __init__( 93 self, 94 binary_path, 95 benchmarks_path, 96 interpreter_args: Optional[str] = None, 97 interpreter_name: Optional[str] = None, 98 benchmark_args: Optional[str] = None, 99 ): 100 if binary_path == Interpreter.CPYTHON: 101 # Running locally (probably from a unit test) 102 # This could stand to be refactored 103 self.name = Interpreter.CPYTHON 104 self.binary_path = Interpreter.CPYTHON_PATH 105 self.benchmarks_path = benchmarks_path 106 else: 107 self.name = binary_path.rsplit("/", 2)[-3] 108 self.binary_path = binary_path 109 directory = f"{binary_path.rsplit('/', 1)[0]}" 110 if not os.path.isabs(benchmarks_path): 111 benchmarks_path = os.path.normpath(f"{directory}/../{benchmarks_path}") 112 self.benchmarks_path = benchmarks_path 113 114 if interpreter_name is not None: 115 self.name = interpreter_name 116 117 self.available_benchmarks = self.discover_benchmarks() 118 self.interpreter_cmd = [self.binary_path] 119 self.interpreter_args = [] 120 if interpreter_args is not None: 121 self.interpreter_args.extend(shlex.split(interpreter_args)) 122 self.interpreter_cmd.extend(self.interpreter_args) 123 if benchmark_args is not None: 124 self.benchmark_args = shlex.split(benchmark_args) 125 else: 126 self.benchmark_args = [] 127 128 def __repr__(self): 129 return f"<Interpreter {self.binary_path!r}>" 130 131 def create_benchmark_from_file(self, benchmark_file): 132 benchmark_path, _, name_and_ext = benchmark_file.rpartition("/") 133 name, _, ext = name_and_ext.rpartition(".") 134 benchmark = Benchmark(benchmark_path, name, f".{ext}") 135 return benchmark 136 137 def create_benchmark_from_dir(self, benchmark_dir): 138 benchmark_path, _, name = benchmark_dir.rpartition("/") 139 benchmark = Benchmark(benchmark_path, name, "") 140 return benchmark 141 142 def discover_benchmarks(self): 143 discovered_benchmarks = [] 144 for f in os.listdir(self.benchmarks_path): 145 path = f"{self.benchmarks_path}/{f}" 146 if os.path.isfile(path) and path.endswith(".py"): 147 b = self.create_benchmark_from_file(path) 148 discovered_benchmarks.append(b) 149 elif os.path.isdir(path) and ( 150 "__pycache__" not in path and "data" not in path and "django" 151 not in path 152 ): 153 b = self.create_benchmark_from_dir(path) 154 discovered_benchmarks.append(b) 155 return discovered_benchmarks 156 157 158class PyroBenchmarkSuite: 159 def arg_parser(self): 160 parser = argparse.ArgumentParser( 161 description="Pyro benchmark suite", 162 formatter_class=argparse.RawTextHelpFormatter, 163 ) 164 parser.add_argument("--verbose", "-v", action="store_true") 165 parser.add_argument( 166 "--json", action="store_true", help=f"Print the data in a json format" 167 ) 168 interpreter_help = f""" 169Specify interpreter(s) to use: 170 171-i /path/to/python 172 """ 173 parser.add_argument( 174 "--interpreter", 175 "-i", 176 metavar="INTERPRETER", 177 dest="interpreters", 178 type=str, 179 action="append", 180 default=[], 181 help=interpreter_help, 182 ) 183 interpreter_args_help = """ 184Specify command-line arguments to pass to an interpreter. Arguments must be 185supplied as a single string. Arguments apply to the corresponding interpreter. 186For example, if you supplied "-i foo -i bar -a '-X debug'" then the arguments 187'-X debug' would apply to interpreter 'foo', while interpreter 'bar' would 188have no additional arguments. 189""" 190 parser.add_argument( 191 "--interpreter-args", 192 "-a", 193 metavar="INTERPRETER_ARGS", 194 dest="interpreter_args", 195 type=str, 196 action="append", 197 default=[], 198 help=interpreter_args_help, 199 ) 200 interpreter_names_help = """ 201Specify the name that should be used when reporting benchmark results for an 202interpreter. Each name applies to the corresponding interpreter, in order. 203The value for interpreter is used if no display name is provided. For example, 204if you supplied "-i foo -i bar -n 'Foo interp'" then the results for interpreter 205'foo' would be displayed as 'Foo interp', while the results for interpreter 'bar' 206would be displayed as 'bar'. This is useful if you want to use the same interpreter 207with different arguments. 208""" 209 parser.add_argument( 210 "--interpreter-name", 211 "-n", 212 metavar="INTERPRETER_NAME", 213 dest="interpreter_names", 214 type=str, 215 action="append", 216 default=[], 217 help=interpreter_names_help, 218 ) 219 benchmark_args_help = """ 220Specify command-line arguments to pass to the benchmarks. Arguments must be 221supplied as a single string. Arguments apply to all the benchmarks. 222For example, if you supplied "-i lmao -i xyz -b foo -b bar --benchmark-args= 223--benchmark-args='-X debug'" then the arguments '-X debug' would apply to all 224the benchmarks run under interpreter 'xyz' and none for interpreter 'lmao'. 225""" 226 parser.add_argument( 227 "--benchmark-args", 228 metavar="BENCHMARK_ARGS", 229 dest="benchmark_args", 230 type=str, 231 action="append", 232 default=[], 233 help=benchmark_args_help, 234 ) 235 benchmarks_path_help = f""" 236Specify benchmarks_path(s) to use. This must match with the interpreters used 237 238-p /path/to/benchmarks 239 240 """ 241 parser.add_argument( 242 "--path", 243 "-p", 244 metavar="BENCHMARK_PATH", 245 dest="benchmarks_path", 246 type=str, 247 action="append", 248 default=[], 249 help=benchmarks_path_help, 250 ) 251 benchmark_help = f""" 252The benchmark that you wish to run. Use repeatedly 253to select more than one benchmark: 254 255-b richards 256 257Default: all 258 259 """ 260 parser.add_argument( 261 "--benchmark", 262 "-b", 263 metavar="BENCHMARK", 264 dest="benchmarks", 265 type=str, 266 action="append", 267 default=[], 268 help=benchmark_help, 269 ) 270 parser = add_tools_arguments(parser) 271 return parser 272 273 def start_benchmarks(self, args): 274 log.info(f"Verifying benchmark arguments") 275 276 # Check that at least one tool was selected 277 if not args.tools: 278 raise Exception("At least one `--tool` should be specified") 279 280 # Check that at least one interpreter was selected 281 if not args.interpreters: 282 raise Exception("At least one `--interpreter` should be specified") 283 284 # Check that benchmarks path matches the number of interpreters 285 if len(args.benchmarks_path) != len(args.interpreters): 286 raise Exception("The number of --interpreter and --path should match") 287 288 if len(args.interpreter_args) > len(args.interpreters): 289 raise Exception( 290 "The number of interpreter arguments cannot exceed the number" 291 " of interpreters" 292 ) 293 294 assert len(args.interpreters) > 0 295 interpreters = [] 296 for i, interp in enumerate(args.interpreters): 297 interp_args = None 298 if i < len(args.interpreter_args): 299 interp_args = args.interpreter_args[i] 300 interp_name = None 301 if i < len(args.interpreter_names): 302 interp_name = args.interpreter_names[i] 303 benchmark_args = None 304 if i < len(args.benchmark_args): 305 benchmark_args = args.benchmark_args[i] 306 interpreters.append( 307 Interpreter( 308 interp, 309 args.benchmarks_path[i], 310 interp_args, 311 interp_name, 312 benchmark_args, 313 ) 314 ) 315 316 # If no benchmark is defined, add all of them 317 if not args.benchmarks: 318 for interpreter in interpreters: 319 to_run = [b for b in interpreter.available_benchmarks] 320 interpreter.benchmarks_to_run = sorted(to_run) 321 else: 322 for interpreter in interpreters: 323 interpreter.benchmarks_to_run = [] 324 for b in interpreter.available_benchmarks: 325 if b.name in args.benchmarks: 326 interpreter.benchmarks_to_run.append(b) 327 interpreter.benchmarks_to_run = sorted(interpreter.benchmarks_to_run) 328 329 # Try to run the benchmarks of the interpreter with the least benchmarks 330 benchmarks_to_run = interpreters[0].benchmarks_to_run 331 for interpreter in interpreters: 332 if len(interpreter.benchmarks_to_run) < len(benchmarks_to_run): 333 benchmarks_to_run = interpreter.benchmarks_to_run 334 for interpreter in interpreters: 335 temp_to_run = interpreter.benchmarks_to_run 336 for benchmark in interpreter.benchmarks_to_run: 337 if benchmark not in benchmarks_to_run: 338 log.info(f"Removing: {benchmark}") 339 temp_to_run.remove(benchmark) 340 interpreter.benchmarks_to_run = temp_to_run 341 342 # Only run if all interpreters have the same benchmarks to run 343 assert len(benchmarks_to_run) > 0 344 log.info(f"Will run the following benchmarks: {benchmarks_to_run}") 345 for interpreter in interpreters: 346 if benchmarks_to_run != interpreter.benchmarks_to_run: 347 raise Exception( 348 "Can't run parallel tools. The interpreters " 349 "have different available benchmarks: " 350 f"{benchmarks_to_run} vs {interpreter.benchmarks_to_run}" 351 ) 352 353 print_json = args.json 354 355 log.info(f"Running benchmarks with args: {args}") 356 runner = BenchmarkRunner(vars(args), interpreters) 357 try: 358 benchmark_results = runner.run_benchmarks() 359 except subprocess.CalledProcessError as cpe: 360 raise RuntimeError( 361 f"{cpe}\n\nstdout:\n{cpe.stdout}\n\nstderr:\n{cpe.stderr}" 362 ) 363 log.info(benchmark_results) 364 365 if print_json: 366 json_output = json.dumps(benchmark_results) 367 if __name__ == "__main__": 368 print(json_output) 369 return json_output 370 371 if not print_json: 372 if __name__ == "__main__": 373 print(build_table(benchmark_results)) 374 return benchmark_results 375 376 377def main(argv): 378 suite = PyroBenchmarkSuite() 379 parser = suite.arg_parser() 380 args = parser.parse_args(argv) 381 logging.basicConfig( 382 level=logging.DEBUG if args.verbose else logging.WARN, format="%(message)s" 383 ) 384 return suite.start_benchmarks(args) 385 386 387if __name__ == "__main__": 388 main(sys.argv[1:])