Add BenchmarkSuite to load benchmarks. (#8753)

diff --git a/build_tools/benchmarks/common/CMakeLists.txt b/build_tools/benchmarks/common/CMakeLists.txt
index 315e08e..f9e2e94 100644
--- a/build_tools/benchmarks/common/CMakeLists.txt
+++ b/build_tools/benchmarks/common/CMakeLists.txt
@@ -31,12 +31,21 @@
     "benchmark_config_test.py"
 )
 
+iree_py_test(
+  NAME
+    benchmark_suite_test
+  SRCS
+    "benchmark_suite_test.py"
+)
+
 # TODO(#8708): Temporary solution to fix python path for tests.
 set_property(TEST "build_tools/benchmarks/common/linux_device_utils_test"
     APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${BENCHMARKS_TOOL_PYTHON_DIR}:$ENV{PYTHONPATH}")
 set_property(TEST "build_tools/benchmarks/common/common_arguments_test"
-  APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${BENCHMARKS_TOOL_PYTHON_DIR}:$ENV{PYTHONPATH}")
+    APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${BENCHMARKS_TOOL_PYTHON_DIR}:$ENV{PYTHONPATH}")
 set_property(TEST "build_tools/benchmarks/common/benchmark_config_test"
-  APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${BENCHMARKS_TOOL_PYTHON_DIR}:$ENV{PYTHONPATH}")
+    APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${BENCHMARKS_TOOL_PYTHON_DIR}:$ENV{PYTHONPATH}")
+set_property(TEST "build_tools/benchmarks/common/benchmark_suite_test"
+    APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${BENCHMARKS_TOOL_PYTHON_DIR}:$ENV{PYTHONPATH}")
 
 endif()
diff --git a/build_tools/benchmarks/common/benchmark_suite.py b/build_tools/benchmarks/common/benchmark_suite.py
index bd72617..564a657 100644
--- a/build_tools/benchmarks/common/benchmark_suite.py
+++ b/build_tools/benchmarks/common/benchmark_suite.py
@@ -29,16 +29,150 @@
         └── <compiled-iree-model>-<sha1>.vmfb
 """
 
-import re
+import collections
 import os
+import re
 
-from typing import List, Optional, Sequence
-
-from .benchmark_definition import DeviceInfo, BenchmarkInfo
+from common.benchmark_definition import DeviceInfo, BenchmarkInfo
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Sequence, Tuple
 
 # All benchmarks' relative path against root build directory.
 BENCHMARK_SUITE_REL_PATH = "benchmark_suites"
 
+MODEL_TOOLFILE_NAME = "tool"
+
+
+@dataclass
+class BenchmarkCase:
+  """Represents a benchmark case.
+  
+    model_name_with_tags: the source model with tags, e.g., 'MobileSSD-fp32'.
+    bench_mode: the benchmark mode, e.g., '1-thread,big-core'.
+    target_arch: the target CPU/GPU architature, e.g., 'GPU-Adreno'.
+    driver: the IREE driver to run with, e.g., 'dylib'.
+    benchmark_case_dir: the path to benchmark case directory.
+    benchmark_tool_name: the benchmark tool, e.g., 'iree-benchmark-module'.
+  """
+
+  model_name_with_tags: str
+  bench_mode: str
+  target_arch: str
+  driver: str
+  benchmark_case_dir: str
+  benchmark_tool_name: str
+
+
+class BenchmarkSuite(object):
+  """Represents the benchmarks in benchmark suite directory."""
+
+  def __init__(self, suite_map: Dict[str, List[BenchmarkCase]]):
+    """Construct a benchmark suite.
+    
+    Args:
+      suites: the map of benchmark cases keyed by category directories.
+    """
+    self.suite_map = suite_map
+    self.category_map = dict((os.path.basename(category_dir), category_dir)
+                             for category_dir in self.suite_map.keys())
+
+  def list_categories(self) -> List[Tuple[str, str]]:
+    """Returns all categories and their directories.
+    
+    Returns:
+      A tuple of (category name, category dir).
+    """
+    category_list = [(name, path) for name, path in self.category_map.items()]
+    # Fix the order of category list.
+    category_list.sort(key=lambda category: category[0])
+    return category_list
+
+  def filter_benchmarks_for_category(
+      self,
+      category: str,
+      available_drivers: Sequence[str],
+      cpu_target_arch_filter: str,
+      gpu_target_arch_filter: str,
+      driver_filter: Optional[str] = None,
+      mode_filter: Optional[str] = None,
+      model_name_filter: Optional[str] = None) -> Sequence[BenchmarkCase]:
+    """Filters benchmarks in a specific category for the given device.
+      Args:
+        category: the specific benchmark category.
+        available_drivers: list of drivers supported by the tools.
+        cpu_target_arch_filter: CPU target architecture filter regex.
+        gpu_target_arch_filter: GPU target architecture filter regex.
+        driver_filter: driver filter regex.
+        mode_filter: benchmark mode regex.
+        model_name_filter: model name regex.
+      Returns:
+        A list of matched benchmark cases.
+    """
+
+    category_dir = self.category_map.get(category)
+    if category_dir is None:
+      return []
+
+    chosen_cases = []
+    for benchmark_case in self.suite_map[category_dir]:
+      matched_driver = (benchmark_case.driver in available_drivers) and (
+          driver_filter is None or
+          re.match(driver_filter, benchmark_case.driver) is not None)
+      matched_arch = (re.match(cpu_target_arch_filter,
+                               benchmark_case.target_arch) is not None or
+                      re.match(gpu_target_arch_filter,
+                               benchmark_case.target_arch) is not None)
+      matched_mode = (mode_filter is None or re.match(
+          mode_filter, benchmark_case.bench_mode) is not None)
+
+      # For backward compatibility, model_name_filter matches against the string:
+      #   <model name with tags>/<benchmark case name>
+      model_and_case_name = f"{benchmark_case.model_name_with_tags}/{os.path.basename(benchmark_case.benchmark_case_dir)}"
+      matched_model_name = (model_name_filter is None or re.match(
+          model_name_filter, model_and_case_name) is not None)
+
+      if (matched_driver and matched_arch and matched_model_name and
+          matched_mode):
+        chosen_cases.append(benchmark_case)
+
+    return chosen_cases
+
+  @staticmethod
+  def load_from_benchmark_suite_dir(benchmark_suite_dir: str):
+    """Scans and loads the benchmarks under the directory."""
+
+    suite_map: Dict[str, List[BenchmarkCase]] = collections.defaultdict(list)
+    for benchmark_case_dir, _, _ in os.walk(benchmark_suite_dir):
+      model_dir, benchmark_name = os.path.split(benchmark_case_dir)
+      # Take the benchmark directory name and see if it matches the benchmark
+      # naming convention:
+      #   <iree-driver>__<target-architecture>__<benchmark_mode>
+      segments = benchmark_name.split("__")
+      if len(segments) != 3 or not segments[0].startswith("iree-"):
+        continue
+
+      iree_driver, target_arch, bench_mode = segments
+      iree_driver = iree_driver[len("iree-"):].lower()
+      target_arch = target_arch.lower()
+
+      # The path of model_dir is expected to be:
+      #   <benchmark_suite_dir>/<category>/<model_name>-<model_tags>
+      category_dir, model_name_with_tags = os.path.split(model_dir)
+
+      with open(os.path.join(benchmark_case_dir, MODEL_TOOLFILE_NAME),
+                "r") as f:
+        tool_name = f.read().strip()
+
+      suite_map[category_dir].append(
+          BenchmarkCase(model_name_with_tags=model_name_with_tags,
+                        bench_mode=bench_mode,
+                        target_arch=target_arch,
+                        driver=iree_driver,
+                        benchmark_case_dir=benchmark_case_dir,
+                        benchmark_tool_name=tool_name))
+
+    return BenchmarkSuite(suite_map=suite_map)
+
 
 def compose_info_object(device_info: DeviceInfo, benchmark_category_dir: str,
                         benchmark_case_dir: str) -> BenchmarkInfo:
@@ -137,7 +271,6 @@
     chosen = False
     if matched_driver and matched_arch and matched_model_name and matched_mode:
       matched_benchmarks.append(root)
-      chosen = True
 
     if verbose:
       print(f"dir: {root}")
diff --git a/build_tools/benchmarks/common/benchmark_suite_test.py b/build_tools/benchmarks/common/benchmark_suite_test.py
new file mode 100644
index 0000000..68085bc
--- /dev/null
+++ b/build_tools/benchmarks/common/benchmark_suite_test.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+# Copyright 2022 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import os
+import tempfile
+import unittest
+
+from common.benchmark_suite import BenchmarkCase, BenchmarkSuite
+
+
+class BenchmarkSuiteTest(unittest.TestCase):
+
+  def test_list_categories(self):
+    suite = BenchmarkSuite({
+        "suite/TFLite": [],
+        "suite/PyTorch": [],
+    })
+
+    self.assertEqual(suite.list_categories(), [("PyTorch", "suite/PyTorch"),
+                                               ("TFLite", "suite/TFLite")])
+
+  def test_filter_benchmarks_for_category(self):
+    case1 = BenchmarkCase(model_name_with_tags="deepnet",
+                          bench_mode="1-thread,full-inference",
+                          target_arch="ARMv8",
+                          driver="dylib",
+                          benchmark_case_dir="case1",
+                          benchmark_tool_name="tool")
+    case2 = BenchmarkCase(model_name_with_tags="deepnetv2-f32",
+                          bench_mode="full-inference",
+                          target_arch="Mali",
+                          driver="vulkan",
+                          benchmark_case_dir="case2",
+                          benchmark_tool_name="tool")
+    suite = BenchmarkSuite({
+        "suite/TFLite": [case1, case2],
+    })
+
+    both_benchmarks = suite.filter_benchmarks_for_category(
+        category="TFLite",
+        available_drivers=["dylib", "vulkan"],
+        cpu_target_arch_filter="ARMv8",
+        gpu_target_arch_filter="Mali",
+        driver_filter=None,
+        mode_filter=".*full-inference.*",
+        model_name_filter="deepnet.*")
+    gpu_benchmarks = suite.filter_benchmarks_for_category(
+        category="TFLite",
+        available_drivers=["dylib", "vulkan"],
+        cpu_target_arch_filter="Unknown",
+        gpu_target_arch_filter="Mali",
+        driver_filter="vulkan",
+        mode_filter=".*full-inference.*",
+        model_name_filter="deepnet.*/case2")
+
+    self.assertEqual(both_benchmarks, [case1, case2])
+    self.assertEqual(gpu_benchmarks, [case2])
+
+  def test_filter_benchmarks_for_nonexistent_category(self):
+    suite = BenchmarkSuite({
+        "suite/TFLite": [],
+    })
+
+    benchmarks = suite.filter_benchmarks_for_category(
+        category="PyTorch",
+        available_drivers=[],
+        cpu_target_arch_filter="ARMv8",
+        gpu_target_arch_filter="Mali-G78")
+
+    self.assertEqual(benchmarks, [])
+
+  def test_load_from_benchmark_suite_dir(self):
+    with tempfile.TemporaryDirectory() as tmp_dir:
+      tflite_dir = os.path.join(tmp_dir, "TFLite")
+      pytorch_dir = os.path.join(tmp_dir, "PyTorch")
+      case1 = BenchmarkSuiteTest.__create_bench(tflite_dir,
+                                                model="deepnet",
+                                                bench_mode="4-thread,full",
+                                                target_arch="cpu-armv8",
+                                                driver="dylib",
+                                                tool="run-cpu-bench")
+      case2 = BenchmarkSuiteTest.__create_bench(pytorch_dir,
+                                                model="deepnetv2",
+                                                bench_mode="full-inference",
+                                                target_arch="gpu-mali",
+                                                driver="vulkan",
+                                                tool="run-gpu-bench")
+
+      suite = BenchmarkSuite.load_from_benchmark_suite_dir(tmp_dir)
+
+      self.assertEqual(suite.list_categories(), [("PyTorch", pytorch_dir),
+                                                 ("TFLite", tflite_dir)])
+      self.assertEqual(
+          suite.filter_benchmarks_for_category(
+              category="PyTorch",
+              available_drivers=["vulkan"],
+              cpu_target_arch_filter="cpu-armv8",
+              gpu_target_arch_filter="gpu-mali"), [case2])
+
+  @staticmethod
+  def __create_bench(dir_path: str, model: str, bench_mode: str,
+                     target_arch: str, driver: str, tool: str):
+    case_name = f"iree-{driver}__{target_arch}__{bench_mode}"
+    bench_path = os.path.join(dir_path, model, case_name)
+    os.makedirs(bench_path)
+    with open(os.path.join(bench_path, "tool"), "w") as f:
+      f.write(tool)
+
+    return BenchmarkCase(model_name_with_tags=model,
+                         bench_mode=bench_mode,
+                         target_arch=target_arch,
+                         driver=driver,
+                         benchmark_case_dir=bench_path,
+                         benchmark_tool_name=tool)
+
+
+if __name__ == "__main__":
+  unittest.main()