app.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. # MODULE: grass.benchmark
  2. #
  3. # AUTHOR(S): Vaclav Petras <wenzeslaus gmail com>
  4. #
  5. # PURPOSE: Benchmarking for GRASS GIS modules
  6. #
  7. # COPYRIGHT: (C) 2021 Vaclav Petras, and by the GRASS Development Team
  8. #
  9. # This program is free software under the GNU General Public
  10. # License (>=v2). Read the file COPYING that comes with GRASS
  11. # for details.
  12. """CLI for the benchmark package"""
  13. import argparse
  14. import sys
  15. from pathlib import Path
  16. from grass.benchmark import (
  17. join_results_from_files,
  18. load_results_from_file,
  19. nprocs_plot,
  20. num_cells_plot,
  21. save_results_to_file,
  22. )
  23. class CliUsageError(ValueError):
  24. """Raised when error is in the command line arguments.
  25. Used when the error is discovered only after argparse parsed the arguments.
  26. """
  27. # ArgumentError from argparse may work too, but it is not documented and
  28. # takes a reference argument which we don't have access to after the parse step.
  29. def join_results_cli(args):
  30. """Translate CLI parser result to API calls."""
  31. if args.prefixes and len(args.results) != len(args.prefixes):
  32. raise CliUsageError(
  33. f"Number of prefixes ({len(args.prefixes)}) needs to be the same"
  34. f" as the number of input result files ({len(args.results)})"
  35. )
  36. def select_only(result):
  37. return result.label == args.only
  38. if args.only:
  39. select_function = select_only
  40. else:
  41. select_function = None
  42. results = join_results_from_files(
  43. source_filenames=args.results,
  44. prefixes=args.prefixes,
  45. select=select_function,
  46. prefixes_as_labels=args.re_label,
  47. )
  48. save_results_to_file(results, args.output)
  49. def plot_nprocs_cli(args):
  50. """Translate CLI parser result to API calls."""
  51. results = load_results_from_file(args.input)
  52. nprocs_plot(
  53. results.results,
  54. filename=args.output,
  55. title=args.title,
  56. )
  57. def plot_cells_cli(args):
  58. """Translate CLI parser result to API calls."""
  59. results = load_results_from_file(args.input)
  60. num_cells_plot(
  61. results.results,
  62. filename=args.output,
  63. title=args.title,
  64. show_resolution=args.resolutions,
  65. )
  66. def get_executable_name():
  67. """Get name of the executable and module.
  68. This is a workaround for Python issue:
  69. argparse support for "python -m module" in help
  70. https://bugs.python.org/issue22240
  71. """
  72. executable = Path(sys.executable).stem
  73. return f"{executable} -m grass.benchmark"
  74. class ExtendAction(argparse.Action):
  75. """Support for agrparse action="extend" before Python 3.8
  76. Each parser instance needs the action to be registered.
  77. """
  78. # pylint: disable=too-few-public-methods
  79. def __call__(self, parser, namespace, values, option_string=None):
  80. items = getattr(namespace, self.dest) or []
  81. items.extend(values)
  82. setattr(namespace, self.dest, items)
  83. def add_subcommand_parser(subparsers, name, description):
  84. """Add parser for a subcommand into subparsers."""
  85. # help is in parent's help, description in subcommand's help.
  86. return subparsers.add_parser(name, help=description, description=description)
  87. def add_subparsers(parser, dest):
  88. """Add subparsers in a unified way.
  89. Uses title 'subcommands' for the list of commands
  90. (instead of the 'positional' which is the default).
  91. The *dest* should be 'command', 'subcommand', etc. with appropriate nesting.
  92. """
  93. if sys.version_info < (3, 7):
  94. # required as parameter is only in >=3.7.
  95. return parser.add_subparsers(title="subcommands", dest=dest)
  96. return parser.add_subparsers(title="subcommands", required=True, dest=dest)
  97. def add_results_subcommand(parent_subparsers):
  98. """Add results subcommand."""
  99. main_parser = add_subcommand_parser(
  100. parent_subparsers, "results", description="Manipulate results"
  101. )
  102. main_subparsers = add_subparsers(main_parser, dest="subcommand")
  103. join = main_subparsers.add_parser("join", help="Join results")
  104. join.add_argument("results", help="Files with results", nargs="*", metavar="file")
  105. join.add_argument("output", help="Output file", metavar="output_file")
  106. if sys.version_info < (3, 8):
  107. join.register("action", "extend", ExtendAction)
  108. join.add_argument(
  109. "--prefixes",
  110. help="Add prefixes to result labels per file",
  111. action="extend",
  112. nargs="*",
  113. metavar="text",
  114. )
  115. join.add_argument(
  116. "--only",
  117. help="Select only results with matching label",
  118. metavar="label",
  119. )
  120. join.add_argument(
  121. "--re-label",
  122. help="Use prefixes as the new labels",
  123. action="store_true",
  124. )
  125. join.set_defaults(handler=join_results_cli)
  126. def add_plot_io_arguments(parser):
  127. """Add input and output arguments to *parser*."""
  128. parser.add_argument("input", help="file with results (JSON)", metavar="input_file")
  129. parser.add_argument(
  130. "output", help="output file (e.g., PNG)", nargs="?", metavar="output_file"
  131. )
  132. def add_plot_title_argument(parser):
  133. """Add title argument to *parser*."""
  134. parser.add_argument(
  135. "--title",
  136. help="Title for the plot",
  137. metavar="text",
  138. )
  139. def add_plot_subcommand(parent_subparsers):
  140. """Add plot subcommand."""
  141. main_parser = add_subcommand_parser(
  142. parent_subparsers, "plot", description="Plot results"
  143. )
  144. main_subparsers = add_subparsers(main_parser, dest="subcommand")
  145. join = main_subparsers.add_parser("cells", help="Plot for variable number of cells")
  146. add_plot_io_arguments(join)
  147. add_plot_title_argument(join)
  148. join.add_argument(
  149. "--resolutions",
  150. help="Use resolutions for x axis instead of cell count",
  151. action="store_true",
  152. )
  153. join.set_defaults(handler=plot_cells_cli)
  154. nprocs = main_subparsers.add_parser(
  155. "nprocs", help="Plot for variable number of processing elements"
  156. )
  157. add_plot_io_arguments(nprocs)
  158. add_plot_title_argument(nprocs)
  159. nprocs.set_defaults(handler=plot_nprocs_cli)
  160. def define_arguments():
  161. """Define top level parser and create subparsers."""
  162. parser = argparse.ArgumentParser(
  163. description="Process results from module benchmarks.",
  164. prog=get_executable_name(),
  165. )
  166. subparsers = add_subparsers(parser, dest="command")
  167. add_results_subcommand(subparsers)
  168. add_plot_subcommand(subparsers)
  169. return parser
  170. def main(args=None):
  171. """Define and parse command line parameters then run the appropriate handler."""
  172. parser = define_arguments()
  173. args = parser.parse_args(args)
  174. try:
  175. args.handler(args)
  176. except CliUsageError as error:
  177. # Report a usage error and exit.
  178. sys.exit(f"ERROR: {error}")
  179. if __name__ == "__main__":
  180. main()