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