yams/tests/benchmarks/benchmark_cli.h

204 lines
6.7 KiB
C++

#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace yams::benchmark {
struct BenchmarkCliConfig {
// Core execution controls
std::size_t warmupIterations{3};
std::size_t iterations{10};
bool verbose{true};
bool trackMemory{true};
// Selection
std::vector<std::string> filters;
std::vector<std::string> exactFilters;
// Output
std::filesystem::path outDir{"bench_results"};
std::optional<std::filesystem::path> outputFile;
// Archiving
bool archive{true};
std::filesystem::path archiveDir{"bench_results/archive"};
// RNG seeding (benchmarks may opt-in)
std::optional<std::uint64_t> seed;
};
inline void printBenchmarkUsage(std::string_view exeName) {
// Keep this minimal; CLI output is consumed by humans.
std::cout << "Usage: " << exeName << " [options]\n\n"
<< "Options:\n"
<< " --warmup N Warmup iterations (default: 3)\n"
<< " --iterations N Benchmark iterations (default: 10)\n"
<< " --quiet Disable per-benchmark progress output\n"
<< " --verbose Enable per-benchmark progress output (default)\n"
<< " --no-memory Disable memory tracking\n"
<< " --filter PATTERN Run only benchmarks whose name contains PATTERN\n"
<< " (repeatable)\n"
<< " --exact-filter NAME Run only benchmarks whose name exactly matches NAME\n"
<< " (repeatable)\n"
<< " --out-dir DIR Output directory (default: bench_results)\n"
<< " --output FILE Append JSONL results to FILE\n"
<< " --seed N Seed for RNG (optional; benchmark-dependent)\n"
<< " --archive Archive results to bench_results/archive (default)\n"
<< " --no-archive Disable archiving\n"
<< " --archive-dir DIR Archive directory\n"
<< " --help Show this help\n";
}
inline BenchmarkCliConfig parseBenchmarkArgs(int argc, char** argv) {
BenchmarkCliConfig cfg;
auto toSizeT = [](const char* s) -> std::size_t {
return static_cast<std::size_t>(std::stoull(std::string(s)));
};
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i] ? std::string(argv[i]) : std::string();
if (arg == "--help" || arg == "-h") {
printBenchmarkUsage(argv[0] ? argv[0] : "benchmark");
std::exit(0);
}
if (arg == "--quiet") {
cfg.verbose = false;
continue;
}
if (arg == "--verbose") {
cfg.verbose = true;
continue;
}
if (arg == "--no-memory") {
cfg.trackMemory = false;
continue;
}
if (arg == "--archive") {
cfg.archive = true;
continue;
}
if (arg == "--no-archive") {
cfg.archive = false;
continue;
}
auto needValue = [&](const char* flag) -> const char* {
if (i + 1 >= argc || !argv[i + 1]) {
throw std::runtime_error(std::string("Missing value for ") + flag);
}
return argv[++i];
};
if (arg == "--warmup") {
cfg.warmupIterations = toSizeT(needValue("--warmup"));
} else if (arg == "--iterations") {
cfg.iterations = toSizeT(needValue("--iterations"));
} else if (arg == "--filter") {
cfg.filters.emplace_back(needValue("--filter"));
} else if (arg == "--exact-filter") {
cfg.exactFilters.emplace_back(needValue("--exact-filter"));
} else if (arg == "--out-dir") {
cfg.outDir = std::filesystem::path(needValue("--out-dir"));
} else if (arg == "--output") {
cfg.outputFile = std::filesystem::path(needValue("--output"));
} else if (arg == "--archive-dir") {
cfg.archiveDir = std::filesystem::path(needValue("--archive-dir"));
} else if (arg == "--seed") {
cfg.seed = static_cast<std::uint64_t>(std::stoull(std::string(needValue("--seed"))));
} else {
// Unknown args are ignored for forward-compat across multiple benchmark binaries.
}
}
return cfg;
}
inline bool matchesAnyFilter(std::string_view name, const std::vector<std::string>& filters) {
if (filters.empty()) {
return true;
}
for (const auto& f : filters) {
if (!f.empty() && name.find(f) != std::string_view::npos) {
return true;
}
}
return false;
}
inline bool matchesAnyFilter(std::string_view name, const std::vector<std::string>& filters,
const std::vector<std::string>& exactFilters) {
if (!exactFilters.empty()) {
for (const auto& f : exactFilters) {
if (!f.empty() && name == f) {
return true;
}
}
}
if (!filters.empty()) {
return matchesAnyFilter(name, filters);
}
return exactFilters.empty();
}
inline std::string iso8601UtcNow() {
using clock = std::chrono::system_clock;
const auto now = clock::now();
const auto t = clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H-%M-%SZ", &tm);
return std::string(buf);
}
inline std::optional<std::string> tryReadTextFile(const std::filesystem::path& p) {
std::ifstream in(p);
if (!in) {
return std::nullopt;
}
std::string s;
std::getline(in, s);
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) {
s.pop_back();
}
return s;
}
inline std::optional<std::filesystem::path>
archiveJsonFileBestEffort(const std::filesystem::path& src,
const std::filesystem::path& archiveRoot, std::string_view suiteName) {
std::error_code ec;
if (!std::filesystem::exists(src)) {
return std::nullopt;
}
const std::string stamp = iso8601UtcNow();
std::filesystem::path dstDir = archiveRoot / std::string(suiteName) / stamp;
std::filesystem::create_directories(dstDir, ec);
std::filesystem::path dst = dstDir / src.filename();
std::filesystem::copy_file(src, dst, std::filesystem::copy_options::overwrite_existing, ec);
return dstDir;
}
} // namespace yams::benchmark