plot_parametrized.py
1 #!/usr/bin/env python 2 # /// script 3 # requires-python = ">=3.10" 4 # dependencies = [ 5 # "matplotlib", 6 # "pyqt6", 7 # ] 8 # /// 9 10 """This program shows parametrized `hyperfine` benchmark results as an 11 errorbar plot.""" 12 13 import argparse 14 import json 15 import sys 16 17 import matplotlib.pyplot as plt 18 19 parser = argparse.ArgumentParser(description=__doc__) 20 parser.add_argument("file", help="JSON file with benchmark results", nargs="+") 21 parser.add_argument( 22 "--parameter-name", 23 metavar="name", 24 type=str, 25 help="Deprecated; parameter names are now inferred from benchmark files", 26 ) 27 parser.add_argument( 28 "--log-x", help="Use a logarithmic x (parameter) axis", action="store_true" 29 ) 30 parser.add_argument( 31 "--log-time", help="Use a logarithmic time axis", action="store_true" 32 ) 33 parser.add_argument( 34 "--titles", help="Comma-separated list of titles for the plot legend" 35 ) 36 parser.add_argument("-o", "--output", help="Save image to the given filename.") 37 38 args = parser.parse_args() 39 if args.parameter_name is not None: 40 sys.stderr.write( 41 "warning: --parameter-name is deprecated; names are inferred from " 42 "benchmark results\n" 43 ) 44 45 46 def die(msg): 47 sys.stderr.write(f"fatal: {msg}\n") 48 sys.exit(1) 49 50 51 def extract_parameters(results): 52 """Return `(parameter_name: str, parameter_values: List[float])`.""" 53 if not results: 54 die("no benchmark data to plot") 55 (names, values) = zip(*(unique_parameter(b) for b in results)) 56 names = frozenset(names) 57 if len(names) != 1: 58 die( 59 f"benchmarks must all have the same parameter name, but found: {sorted(names)}" 60 ) 61 return (next(iter(names)), list(values)) 62 63 64 def unique_parameter(benchmark): 65 """Return the unique parameter `(name: str, value: float)`, or die.""" 66 params_dict = benchmark.get("parameters", {}) 67 if not params_dict: 68 die("benchmarks must have exactly one parameter, but found none") 69 if len(params_dict) > 1: 70 die( 71 f"benchmarks must have exactly one parameter, but found multiple: {sorted(params_dict)}" 72 ) 73 [(name, value)] = params_dict.items() 74 return (name, float(value)) 75 76 77 parameter_name = None 78 79 for filename in args.file: 80 with open(filename) as f: 81 results = json.load(f)["results"] 82 83 (this_parameter_name, parameter_values) = extract_parameters(results) 84 if parameter_name is not None and this_parameter_name != parameter_name: 85 die( 86 f"files must all have the same parameter name, but found {parameter_name!r} vs. {this_parameter_name!r}" 87 ) 88 parameter_name = this_parameter_name 89 90 times_mean = [b["mean"] for b in results] 91 times_stddev = [b["stddev"] for b in results] 92 93 plt.errorbar(x=parameter_values, y=times_mean, yerr=times_stddev, capsize=2) 94 95 plt.xlabel(parameter_name) 96 plt.ylabel("Time [s]") 97 98 if args.log_time: 99 plt.yscale("log") 100 else: 101 plt.ylim(0, None) 102 103 if args.log_x: 104 plt.xscale("log") 105 106 if args.titles: 107 plt.legend(args.titles.split(",")) 108 109 if args.output: 110 plt.savefig(args.output) 111 else: 112 plt.show()