瀏覽代碼

libpython: Add plot nprocs to benchmark CLI (#1761)

* Adds new subcommand plot nprocs.
* Uses more general plot title.
* Allows user to select results by label.
* Prefixes can replace labels instead of prefixing them.
* Adds test for plot nprocs (uses nprocs with r.univar, so it is skipped in 8.0).
Vaclav Petras 3 年之前
父節點
當前提交
3130a47d7c

+ 59 - 10
python/grass/benchmark/app.py

@@ -20,6 +20,7 @@ from pathlib import Path
 from grass.benchmark import (
     join_results_from_files,
     load_results_from_file,
+    nprocs_plot,
     num_cells_plot,
     save_results_to_file,
 )
@@ -33,7 +34,6 @@ class CliUsageError(ValueError):
 
     # ArgumentError from argparse may work too, but it is not documented and
     # takes a reference argument which we don't have access to after the parse step.
-    pass
 
 
 def join_results_cli(args):
@@ -43,13 +43,35 @@ def join_results_cli(args):
             f"Number of prefixes ({len(args.prefixes)}) needs to be the same"
             f" as the number of input result files ({len(args.results)})"
         )
+
+    def select_only(result):
+        return result.label == args.only
+
+    if args.only:
+        select_function = select_only
+    else:
+        select_function = None
+
     results = join_results_from_files(
         source_filenames=args.results,
         prefixes=args.prefixes,
+        select=select_function,
+        prefixes_as_labels=args.re_label,
     )
+
     save_results_to_file(results, args.output)
 
 
+def plot_nprocs_cli(args):
+    """Translate CLI parser result to API calls."""
+    results = load_results_from_file(args.input)
+    nprocs_plot(
+        results.results,
+        filename=args.output,
+        title=args.title,
+    )
+
+
 def plot_cells_cli(args):
     """Translate CLI parser result to API calls."""
     results = load_results_from_file(args.input)
@@ -124,9 +146,36 @@ def add_results_subcommand(parent_subparsers):
         nargs="*",
         metavar="text",
     )
+    join.add_argument(
+        "--only",
+        help="Select only results with matching label",
+        metavar="label",
+    )
+    join.add_argument(
+        "--re-label",
+        help="Use prefixes as the new labels",
+        action="store_true",
+    )
     join.set_defaults(handler=join_results_cli)
 
 
+def add_plot_io_arguments(parser):
+    """Add input and output arguments to *parser*."""
+    parser.add_argument("input", help="file with results (JSON)", metavar="input_file")
+    parser.add_argument(
+        "output", help="output file (e.g., PNG)", nargs="?", metavar="output_file"
+    )
+
+
+def add_plot_title_argument(parser):
+    """Add title argument to *parser*."""
+    parser.add_argument(
+        "--title",
+        help="Title for the plot",
+        metavar="text",
+    )
+
+
 def add_plot_subcommand(parent_subparsers):
     """Add plot subcommand."""
     main_parser = add_subcommand_parser(
@@ -135,15 +184,8 @@ def add_plot_subcommand(parent_subparsers):
     main_subparsers = add_subparsers(main_parser, dest="subcommand")
 
     join = main_subparsers.add_parser("cells", help="Plot for variable number of cells")
-    join.add_argument("input", help="file with results (JSON)", metavar="input_file")
-    join.add_argument(
-        "output", help="output file (e.g., PNG)", nargs="?", metavar="output_file"
-    )
-    join.add_argument(
-        "--title",
-        help="Title for the plot",
-        metavar="text",
-    )
+    add_plot_io_arguments(join)
+    add_plot_title_argument(join)
     join.add_argument(
         "--resolutions",
         help="Use resolutions for x axis instead of cell count",
@@ -151,6 +193,13 @@ def add_plot_subcommand(parent_subparsers):
     )
     join.set_defaults(handler=plot_cells_cli)
 
+    nprocs = main_subparsers.add_parser(
+        "nprocs", help="Plot for variable number of processing elements"
+    )
+    add_plot_io_arguments(nprocs)
+    add_plot_title_argument(nprocs)
+    nprocs.set_defaults(handler=plot_nprocs_cli)
+
 
 def define_arguments():
     """Define top level parser and create subparsers."""

+ 10 - 3
python/grass/benchmark/plots.py

@@ -37,7 +37,7 @@ def get_pyplot(to_file):
     return plt
 
 
-def nprocs_plot(results, filename=None):
+def nprocs_plot(results, filename=None, title=None):
     """Plot results from a multiple nprocs (thread) benchmarks.
 
     *results* is a list of individual results from separate benchmarks.
@@ -67,9 +67,16 @@ def nprocs_plot(results, filename=None):
             maxes = [max(i) for i in result.all_times]
             plt.fill_between(x, mins, maxes, color="gray", alpha=0.3)
     plt.legend()
-    axes.set(xticks=sorted(x_ticks))
-    plt.xlabel("Number of cores (threads, processes)")
+    # If there is not many x values, show ticks for each, but use default
+    # ticks when there is a lot of x values.
+    if len(x_ticks) < 10:
+        axes.set(xticks=sorted(x_ticks))
+    plt.xlabel("Number of processing elements (cores, threads, processes)")
     plt.ylabel("Time [s]")
+    if title:
+        plt.title(title)
+    else:
+        plt.title("Execution time by processing elements")
     if filename:
         plt.savefig(filename)
     else:

+ 16 - 4
python/grass/benchmark/results.py

@@ -71,7 +71,7 @@ def load_results_from_file(filename):
         return load_results(file.read())
 
 
-def join_results(results, prefixes=None):
+def join_results(results, prefixes=None, select=None, prefixes_as_labels=False):
     """Join multiple lists of results together
 
     The *results* argument either needs to be a list of result objects
@@ -88,16 +88,28 @@ def join_results(results, prefixes=None):
             # This is the actual list in the full results structure.
             result_list = result_list.results
         for result in result_list:
+            if select and not select(result):
+                continue
             result = copy.deepcopy(result)
             if prefix:
-                result.label = f"{prefix}: {result.label}"
+                if prefixes_as_labels:
+                    result.label = prefix
+                else:
+                    result.label = f"{prefix}: {result.label}"
             joined.append(result)
     return joined
 
 
-def join_results_from_files(source_filenames, prefixes):
+def join_results_from_files(
+    source_filenames, prefixes=None, select=None, prefixes_as_labels=False
+):
     """Join multiple files into one results object."""
     to_merge = []
     for result_file in source_filenames:
         to_merge.append(load_results_from_file(result_file))
-    return join_results(to_merge, prefixes=prefixes)
+    return join_results(
+        to_merge,
+        prefixes=prefixes,
+        select=select,
+        prefixes_as_labels=prefixes_as_labels,
+    )

+ 53 - 11
python/grass/benchmark/testsuite/test_benchmark_cli.py

@@ -12,26 +12,68 @@
 
 """Tests of grass.benchmark CLI"""
 
+import sys
 from pathlib import Path
 from subprocess import DEVNULL
 
-from grass.benchmark import benchmark_resolutions, save_results_to_file
+import grass
+from grass.benchmark import (
+    benchmark_nprocs,
+    benchmark_resolutions,
+    save_results_to_file,
+)
 from grass.benchmark.app import main as benchmark_main
 from grass.gunittest.case import TestCase
 from grass.gunittest.main import test
 from grass.pygrass.modules import Module
 
 
+def remove_file(path):
+    """Remove filename if exists"""
+    if sys.version_info < (3, 8):
+        try:
+            Path(path).unlink()
+        except FileNotFoundError:
+            pass
+    else:
+        Path(path).unlink(missing_ok=True)
+
+
 class TestBenchmarkCLI(TestCase):
     """Tests that benchmarkin CLI works"""
 
-    def test_plot_workflow(self):
-        """Test that plot workflow runs"""
+    json_filename = "plot_test.json"
+    png_filename1 = "plot_test1.png"
+    png_filename2 = "plot_test2.png"
+
+    def tearDown(self):
+        """Remove test files"""
+        remove_file(self.json_filename)
+        remove_file(self.png_filename1)
+        remove_file(self.png_filename2)
+
+    def test_plot_nprocs_workflow(self):
+        """Test that plot nprocs workflow runs"""
+        label = "Standard output"
+        repeat = 4
+        # The benchmark part has only Python API, not CLI.
+        try:
+            result = benchmark_nprocs(
+                module=Module("r.univar", map="elevation", stdout_=DEVNULL, run_=False),
+                label=label,
+                repeat=repeat,
+                max_nprocs=3,
+            )
+        except grass.exceptions.ParameterError:
+            self.skipTest("r.univar without nprocs parameter")
+        save_results_to_file([result], self.json_filename)
+        benchmark_main(["plot", "nprocs", self.json_filename, self.png_filename1])
+        self.assertTrue(Path(self.png_filename1).is_file())
+
+    def test_plot_cells_workflow(self):
+        """Test that plot cells workflow runs"""
         label = "Standard output"
         repeat = 4
-        json_filename = "plot_test.json"
-        png_filename = "plot_test.png"
-        png_filename_resolutions = "plot_test_resolutions.png"
         # The benchmark part has only Python API, not CLI.
         result = benchmark_resolutions(
             module=Module("r.univar", map="elevation", stdout_=DEVNULL, run_=False),
@@ -39,13 +81,13 @@ class TestBenchmarkCLI(TestCase):
             repeat=repeat,
             resolutions=[1000, 500],
         )
-        save_results_to_file([result], json_filename)
-        benchmark_main(["plot", "cells", json_filename, png_filename])
-        self.assertTrue(Path(png_filename).is_file())
+        save_results_to_file([result], self.json_filename)
+        benchmark_main(["plot", "cells", self.json_filename, self.png_filename1])
+        self.assertTrue(Path(self.png_filename1).is_file())
         benchmark_main(
-            ["plot", "cells", "--resolutions", json_filename, png_filename_resolutions]
+            ["plot", "cells", "--resolutions", self.json_filename, self.png_filename2]
         )
-        self.assertTrue(Path(png_filename_resolutions).is_file())
+        self.assertTrue(Path(self.png_filename2).is_file())
 
 
 if __name__ == "__main__":