start adding JSON output -- it's easier to use data in that format
[benchmarks.git] / src / main.cpp
1 #include "all_benchmarks.h"
2 #include "harness.h"
3 #include "json.h"
4 #include <charconv>
5 #include <cstdlib>
6 #include <functional>
7 #include <initializer_list>
8 #include <iostream>
9 #include <limits>
10 #include <map>
11 #include <optional>
12 #include <ostream>
13 #include <string_view>
14 #include <system_error>
15 #include <type_traits>
16 #include <unordered_set>
17 #include <vector>
18
19 using namespace std::literals;
20
21 enum class OptionValueKind
22 {
23 None,
24 Required,
25 };
26
27 class OptionsParser;
28
29 struct Option final
30 {
31 char short_name = '\0';
32 std::string_view long_name = "", description = "";
33 bool required = false;
34 bool all_other_args_not_required = false;
35 OptionValueKind value_kind = OptionValueKind::None;
36 std::function<void(OptionsParser &parser,
37 std::optional<std::string_view> value)>
38 parse_value;
39 bool has_short_name() const
40 {
41 return short_name != '\0';
42 }
43 bool has_long_name() const
44 {
45 return !long_name.empty();
46 }
47 friend std::ostream &operator<<(std::ostream &os, const Option &option)
48 {
49 if (option.has_long_name())
50 {
51 os << "--" << option.long_name;
52 }
53 else if (option.has_short_name())
54 {
55 os << "-" << option.short_name;
56 }
57 else
58 {
59 os << "--<unnamed>";
60 }
61 return os;
62 }
63 };
64
65 class Options final
66 {
67 friend class OptionsParser;
68
69 private:
70 std::vector<Option> options;
71 std::map<char, std::size_t> short_options;
72 std::map<std::string_view, std::size_t> long_options;
73
74 public:
75 Options(std::initializer_list<Option> options_)
76 : options(options_), short_options(), long_options()
77 {
78 for (std::size_t i = 0; i < options.size(); i++)
79 {
80 auto &option = options[i];
81 if (option.has_short_name())
82 short_options[option.short_name] = i;
83 if (option.has_long_name())
84 long_options[option.long_name] = i;
85 }
86 }
87
88 public:
89 std::vector<std::string_view> parse(const char *const *argv) const;
90 };
91
92 class OptionsParser final
93 {
94 private:
95 const Options &options;
96 const char *const *argv;
97 std::string_view argv0;
98 std::vector<bool> seen_options;
99 bool check_required = true;
100 std::optional<std::size_t> current_option_index;
101
102 private:
103 const Option &current_option()
104 {
105 static const Option default_option{};
106 if (current_option_index)
107 {
108 return options.options[*current_option_index];
109 }
110 else
111 {
112 return default_option;
113 }
114 }
115
116 public:
117 OptionsParser(const Options &options, const char *const *argv)
118 : options(options), argv(argv + 1),
119 argv0(argv[0] ? argv[0] : "<none>"),
120 seen_options(options.options.size(), false), current_option_index()
121 {
122 if (!argv[0])
123 {
124 static const char *null[] = {nullptr};
125 this->argv = null;
126 help_and_exit("missing program name");
127 }
128 }
129
130 private:
131 void parse_option(std::size_t option_index, std::string_view prefix,
132 std::string_view name,
133 std::optional<std::string_view> value)
134 {
135 auto &option = options.options[option_index];
136 switch (option.value_kind)
137 {
138 case OptionValueKind::None:
139 if (value)
140 {
141 help_and_exit("value not allowed for ", prefix, name);
142 }
143 break;
144 case OptionValueKind::Required:
145 if (!value)
146 {
147 if (*argv)
148 {
149 value = *argv++;
150 }
151 else
152 {
153 help_and_exit("missing value for ", prefix, name);
154 }
155 }
156 break;
157 }
158 seen_options[option_index] = true;
159 if (option.all_other_args_not_required)
160 check_required = false;
161 current_option_index = option_index;
162 option.parse_value(*this, value);
163 current_option_index = std::nullopt;
164 }
165
166 public:
167 std::vector<std::string_view> parse()
168 {
169 std::vector<std::string_view> retval;
170 constexpr auto npos = std::string_view::npos;
171 while (*argv)
172 {
173 std::string_view arg = *argv++;
174 static constexpr std::string_view long_prefix = "--"sv;
175 static constexpr std::string_view short_prefix = "-"sv;
176 if (arg.rfind(long_prefix, 0) == 0) // it starts with `--`
177 {
178 arg.remove_prefix(long_prefix.size());
179 auto eq = arg.find('=');
180 if (eq == 0)
181 {
182 eq = npos;
183 }
184 if (arg.empty()) // just `--`
185 {
186 while (*argv)
187 {
188 retval.push_back(*argv++);
189 }
190 break;
191 }
192 auto name = arg.substr(0, eq);
193 std::optional<std::string_view> value;
194 if (eq != npos)
195 {
196 value = arg.substr(eq + 1);
197 }
198 auto iter = options.long_options.find(name);
199 if (iter == options.long_options.end())
200 {
201 help_and_exit("unknown option: ", long_prefix, name);
202 }
203 auto option_index = iter->second;
204 parse_option(option_index, long_prefix, name, value);
205 continue;
206 }
207 else if (arg.rfind(short_prefix, 0) == 0) // it starts with `-`
208 {
209 arg.remove_prefix(short_prefix.size());
210 if (arg.empty()) // just `-`
211 {
212 retval.push_back(short_prefix);
213 continue;
214 }
215 while (!arg.empty())
216 {
217 auto name = arg.substr(0, 1);
218 arg.remove_prefix(1);
219 auto iter = options.short_options.find(name[0]);
220 if (iter == options.short_options.end())
221 {
222 help_and_exit("unknown option: ", short_prefix, name);
223 }
224 auto option_index = iter->second;
225 std::optional<std::string_view> value;
226 switch (options.options[option_index].value_kind)
227 {
228 case OptionValueKind::None:
229 break;
230 case OptionValueKind::Required:
231 auto eq = arg.rfind('=', 0);
232 if (eq != npos)
233 {
234 value = arg.substr(eq + 1);
235 }
236 else if (!arg.empty())
237 {
238 value = arg;
239 }
240 arg = "";
241 break;
242 }
243 parse_option(option_index, short_prefix, name, value);
244 }
245 continue;
246 }
247 else
248 {
249 retval.push_back(arg);
250 }
251 }
252 if (check_required)
253 {
254 for (std::size_t i = 0; i < options.options.size(); i++)
255 {
256 auto &option = options.options[i];
257 if (option.required && !seen_options[i])
258 {
259 help_and_exit("missing required option ", option);
260 }
261 }
262 }
263 return retval;
264 }
265 template <typename... Options>
266 [[noreturn]] void help_and_exit(const Options &...error_msg) const
267 {
268 auto &os = sizeof...(error_msg) == 0 ? std::cout : std::cerr;
269 if (sizeof...(error_msg) != 0)
270 {
271 ((os << "Error: ") << ... << error_msg) << std::endl;
272 }
273 os << "Usage: " << argv0;
274 for (auto &option : options.options)
275 {
276 os << " ";
277 if (!option.required)
278 {
279 os << "[";
280 }
281 auto sep = "";
282 if (option.has_short_name())
283 {
284 os << "-" << option.short_name;
285 sep = "|";
286 }
287 if (option.has_long_name())
288 {
289 os << sep << "--" << option.long_name;
290 }
291 switch (option.value_kind)
292 {
293 case OptionValueKind::None:
294 break;
295 // TODO: case OptionValueKind::Optional:
296 case OptionValueKind::Required:
297 os << " <value>";
298 break;
299 }
300 if (!option.required)
301 {
302 os << "]";
303 }
304 }
305 os << std::endl << "Options:" << std::endl;
306 for (auto &option : options.options)
307 {
308 auto sep = "";
309 if (option.has_short_name())
310 {
311 os << "-" << option.short_name;
312 sep = "|";
313 }
314 if (option.has_long_name())
315 {
316 os << sep << "--" << option.long_name;
317 }
318 os << " " << option.description << std::endl;
319 }
320 std::exit(sizeof...(error_msg) == 0 ? 0 : 1);
321 }
322 template <typename Int, typename = void> struct ParseIntLimits;
323 template <typename Int>
324 struct ParseIntLimits<Int, std::enable_if_t<std::is_integral_v<Int>>> final
325 {
326 Int min_value = std::numeric_limits<Int>::min();
327 Int max_value = std::numeric_limits<Int>::max();
328 };
329 template <typename Int>
330 std::enable_if_t<std::is_integral_v<Int>, void> parse_int(
331 std::optional<std::string_view> value, Int &i_value,
332 ParseIntLimits<Int> limits = {})
333 {
334 i_value = Int();
335 if (!value)
336 {
337 help_and_exit("missing value for ", current_option());
338 }
339 auto str = *value;
340 int base = 10;
341 if (0 == str.rfind("0x", 0) || 0 == str.rfind("0X", 0))
342 {
343 base = 16;
344 str.remove_prefix(2);
345 }
346 else if (0 == str.rfind("0o", 0) || 0 == str.rfind("0O", 0))
347 {
348 base = 8;
349 str.remove_prefix(2);
350 }
351 else if (0 == str.rfind("0b", 0) || 0 == str.rfind("0B", 0))
352 {
353 base = 2;
354 str.remove_prefix(2);
355 }
356 std::from_chars_result result = std::from_chars(
357 str.data(), str.data() + str.size(), i_value, base);
358 if (result.ptr != str.data() + str.size())
359 {
360 result.ec = std::errc::invalid_argument;
361 }
362 if (result.ec == std::errc())
363 {
364 if (i_value < limits.min_value || i_value > limits.max_value)
365 {
366 result.ec = std::errc::result_out_of_range;
367 }
368 }
369 if (result.ec == std::errc::result_out_of_range)
370 {
371 help_and_exit("value out of range: ", current_option(), "=",
372 *value);
373 }
374 else if (result.ec != std::errc())
375 {
376 help_and_exit("invalid value for: ", current_option());
377 }
378 }
379 template <typename Int>
380 std::enable_if_t<std::is_integral_v<Int>, void> parse_int(
381 std::optional<std::string_view> value, std::optional<Int> &i_value,
382 bool required = true, ParseIntLimits<Int> limits = {})
383 {
384 if (!required && !value)
385 {
386 i_value = std::nullopt;
387 return;
388 }
389 i_value.emplace();
390 this->parse_int(value, i_value.value(), limits);
391 }
392 };
393
394 inline std::vector<std::string_view> Options::parse(
395 const char *const *argv) const
396 {
397 return OptionsParser(*this, argv).parse();
398 }
399
400 int main(int, char **argv)
401 {
402 Config config{};
403 std::optional<std::unordered_set<std::string>> enabled_benchmarks;
404 Options options{
405 Option{
406 .short_name = 'h',
407 .long_name = "help",
408 .description = "Display usage and exit.",
409 .all_other_args_not_required = true,
410 .parse_value = [](OptionsParser &parser,
411 auto) { parser.help_and_exit(); },
412 },
413 Option{.short_name = 'j',
414 .long_name = "thread-count",
415 .description = "Number of threads to run on",
416 .value_kind = OptionValueKind::Required,
417 .parse_value =
418 [&](OptionsParser &parser, auto value) {
419 parser.parse_int(value, config.thread_count);
420 }},
421 Option{.short_name = 'n',
422 .long_name = "iter-count",
423 .description = "Number of iterations to run per thread",
424 .value_kind = OptionValueKind::Required,
425 .parse_value =
426 [&](OptionsParser &parser, auto value) {
427 parser.parse_int(value, config.iteration_count);
428 }},
429 Option{.long_name = "log2-mem-loc-count",
430 .description =
431 "Log base 2 of the number of memory locations to access",
432 .value_kind = OptionValueKind::Required,
433 .parse_value =
434 [&](OptionsParser &parser, auto value) {
435 parser.parse_int(
436 value, config.log2_memory_location_count,
437 {.max_value =
438 Config::max_sum_log2_mem_loc_count_and_stride -
439 config.log2_stride});
440 }},
441 Option{
442 .long_name = "log2-stride",
443 .description =
444 "Log base 2 of the stride used for accessing memory locations",
445 .value_kind = OptionValueKind::Required,
446 .parse_value =
447 [&](OptionsParser &parser, auto value) {
448 parser.parse_int(
449 value, config.log2_stride,
450 {.max_value =
451 Config::max_sum_log2_mem_loc_count_and_stride -
452 config.log2_memory_location_count});
453 }},
454 Option{
455 .short_name = 'b',
456 .long_name = "bench",
457 .description = "List of benchmarks that should be run",
458 .value_kind = OptionValueKind::Required,
459 .parse_value =
460 [&](OptionsParser &, std::optional<std::string_view> value) {
461 if (!enabled_benchmarks)
462 {
463 enabled_benchmarks.emplace();
464 }
465 enabled_benchmarks->emplace(value.value_or(""));
466 }},
467 Option{.long_name = "json",
468 .description = "Write the output in JSON format",
469 .value_kind = OptionValueKind::None,
470 .parse_value = [&](auto &, auto) { config.use_json = true; }},
471 };
472 OptionsParser parser(options, argv);
473 auto args = parser.parse();
474 if (!args.empty())
475 {
476 parser.help_and_exit("unexpected argument");
477 }
478 auto benchmarks = all_benchmarks(config);
479 if (enabled_benchmarks)
480 {
481 enabled_benchmarks->erase("");
482 enabled_benchmarks->erase("help");
483 enabled_benchmarks->erase("list");
484 if (enabled_benchmarks->empty())
485 {
486 if (config.use_json)
487 {
488 JsonArray names;
489 for (auto &benchmark : benchmarks)
490 {
491 names.push_back(benchmark.name());
492 }
493 std::cout << JsonValue(std::move(names)) << std::endl;
494 }
495 else
496 {
497 std::cout << "Available Benchmarks:\n";
498 for (auto &benchmark : benchmarks)
499 {
500 std::cout << benchmark.name() << "\n";
501 }
502 std::cout << std::endl;
503 }
504 return 0;
505 }
506 std::unordered_set<std::string> unknown_benchmarks =
507 *enabled_benchmarks;
508 for (auto &benchmark : benchmarks)
509 {
510 unknown_benchmarks.erase(benchmark.name());
511 }
512 if (!unknown_benchmarks.empty())
513 {
514 parser.help_and_exit(
515 "unknown benchmark: ", *unknown_benchmarks.begin(),
516 "\nrun with `--bench=list` to see all supported benchmarks.");
517 }
518 }
519 auto thread_cache = BenchHarnessBase::get_thread_cache();
520 for (auto &benchmark : benchmarks)
521 {
522 // FIXME: change output to use JSON when selected
523 if (enabled_benchmarks && !enabled_benchmarks->count(benchmark.name()))
524 {
525 continue;
526 }
527 std::cout << "Running: " << benchmark.name() << std::endl;
528 benchmark.run(config);
529 }
530 std::cout << std::endl;
531 return 0;
532 }