bench.cpp raw

   1  // Copyright (c) 2015-2022 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <bench/bench.h>
   6  
   7  #include <test/util/setup_common.h> // IWYU pragma: keep
   8  #include <tinyformat.h>
   9  #include <util/fs.h>
  10  #include <util/string.h>
  11  
  12  #include <chrono>
  13  #include <compare>
  14  #include <fstream>
  15  #include <functional>
  16  #include <iostream>
  17  #include <map>
  18  #include <ratio>
  19  #include <regex>
  20  #include <set>
  21  #include <stdexcept>
  22  #include <string>
  23  #include <vector>
  24  
  25  using namespace std::chrono_literals;
  26  using util::Join;
  27  
  28  const std::function<void(const std::string&)> G_TEST_LOG_FUN{};
  29  
  30  /**
  31   * Retrieves the available test setup command line arguments that may be used
  32   * in the benchmark. They will be used only if the benchmark utilizes a
  33   * 'BasicTestingSetup' or any child of it.
  34   */
  35  static std::function<std::vector<const char*>()> g_bench_command_line_args{};
  36  const std::function<std::vector<const char*>()> G_TEST_COMMAND_LINE_ARGUMENTS = []() {
  37      return g_bench_command_line_args();
  38  };
  39  
  40  /**
  41   * Retrieve the name of the currently in-use benchmark.
  42   * This is applicable only to benchmarks that utilize the unit test
  43   * framework context setup (e.g. ones using 'MakeNoLogFileContext<TestingSetup>()').
  44   * It places the datadir of each benchmark run within their respective benchmark name.
  45   */
  46  static std::string g_running_benchmark_name;
  47  const std::function<std::string()> G_TEST_GET_FULL_NAME = []() {
  48      return g_running_benchmark_name;
  49  };
  50  
  51  namespace {
  52  
  53  void GenerateTemplateResults(const std::vector<ankerl::nanobench::Result>& benchmarkResults, const fs::path& file, const char* tpl)
  54  {
  55      if (benchmarkResults.empty() || file.empty()) {
  56          // nothing to write, bail out
  57          return;
  58      }
  59      std::ofstream fout{file};
  60      if (fout.is_open()) {
  61          ankerl::nanobench::render(tpl, benchmarkResults, fout);
  62          std::cout << "Created " << file << std::endl;
  63      } else {
  64          std::cout << "Could not write to file " << file << std::endl;
  65      }
  66  }
  67  
  68  } // namespace
  69  
  70  namespace benchmark {
  71  
  72  // map a label to one or multiple priority levels
  73  std::map<std::string, uint8_t> map_label_priority = {
  74      {"high", PriorityLevel::HIGH},
  75      {"low", PriorityLevel::LOW},
  76      {"all", 0xff}
  77  };
  78  
  79  std::string ListPriorities()
  80  {
  81      using item_t = std::pair<std::string, uint8_t>;
  82      auto sort_by_priority = [](item_t a, item_t b){ return a.second < b.second; };
  83      std::set<item_t, decltype(sort_by_priority)> sorted_priorities(map_label_priority.begin(), map_label_priority.end(), sort_by_priority);
  84      return Join(sorted_priorities, ',', [](const auto& entry){ return entry.first; });
  85  }
  86  
  87  uint8_t StringToPriority(const std::string& str)
  88  {
  89      auto it = map_label_priority.find(str);
  90      if (it == map_label_priority.end()) throw std::runtime_error(strprintf("Unknown priority level %s", str));
  91      return it->second;
  92  }
  93  
  94  BenchRunner::BenchmarkMap& BenchRunner::benchmarks()
  95  {
  96      static BenchmarkMap benchmarks_map;
  97      return benchmarks_map;
  98  }
  99  
 100  BenchRunner::BenchRunner(std::string name, BenchFunction func, PriorityLevel level)
 101  {
 102      benchmarks().insert(std::make_pair(name, std::make_pair(func, level)));
 103  }
 104  
 105  void BenchRunner::RunAll(const Args& args)
 106  {
 107      std::regex reFilter(args.regex_filter);
 108      std::smatch baseMatch;
 109  
 110      if (args.sanity_check) {
 111          std::cout << "Running with -sanity-check option, output is being suppressed as benchmark results will be useless." << std::endl;
 112      }
 113  
 114      // Load inner test setup args
 115      g_bench_command_line_args = [&args]() {
 116          std::vector<const char*> ret;
 117          ret.reserve(args.setup_args.size());
 118          for (const auto& arg : args.setup_args) ret.emplace_back(arg.c_str());
 119          return ret;
 120      };
 121  
 122      std::vector<ankerl::nanobench::Result> benchmarkResults;
 123      for (const auto& [name, bench_func] : benchmarks()) {
 124          const auto& [func, priority_level] = bench_func;
 125  
 126          if (!(priority_level & args.priority)) {
 127              continue;
 128          }
 129  
 130          if (!std::regex_match(name, baseMatch, reFilter)) {
 131              continue;
 132          }
 133  
 134          if (args.is_list_only) {
 135              std::cout << name << std::endl;
 136              continue;
 137          }
 138  
 139          Bench bench;
 140          if (args.sanity_check) {
 141              bench.epochs(1).epochIterations(1);
 142              bench.output(nullptr);
 143          }
 144          bench.name(name);
 145          g_running_benchmark_name = name;
 146          if (args.min_time > 0ms) {
 147              // convert to nanos before dividing to reduce rounding errors
 148              std::chrono::nanoseconds min_time_ns = args.min_time;
 149              bench.minEpochTime(min_time_ns / bench.epochs());
 150          }
 151  
 152          if (args.asymptote.empty()) {
 153              func(bench);
 154          } else {
 155              for (auto n : args.asymptote) {
 156                  bench.complexityN(n);
 157                  func(bench);
 158              }
 159              std::cout << bench.complexityBigO() << std::endl;
 160          }
 161  
 162          if (!bench.results().empty()) {
 163              benchmarkResults.push_back(bench.results().back());
 164          }
 165      }
 166  
 167      GenerateTemplateResults(benchmarkResults, args.output_csv, "# Benchmark, evals, iterations, total, min, max, median\n"
 168                                                                 "{{#result}}{{name}}, {{epochs}}, {{average(iterations)}}, {{sumProduct(iterations, elapsed)}}, {{minimum(elapsed)}}, {{maximum(elapsed)}}, {{median(elapsed)}}\n"
 169                                                                 "{{/result}}");
 170      GenerateTemplateResults(benchmarkResults, args.output_json, ankerl::nanobench::templates::json());
 171  }
 172  
 173  } // namespace benchmark
 174