/ thirdparty / hyperfine / scripts / plot_parametrized.py
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()