yams/tests/benchmarks/daemon_warm_latency_main.cpp
2026-08-04 21:31:39 -04:00

265 lines
9.6 KiB
C++

#include <spdlog/spdlog.h>
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#include "../common/env_compat.h"
#include "../common/test_helpers_catch2.h"
#include "bench_utils.h"
#include <yams/compat/unistd.h>
#include <yams/daemon/daemon.h>
namespace fs = std::filesystem;
using steady_clock_t = std::chrono::steady_clock;
using namespace std::chrono_literals;
namespace {
struct TimingSample {
long long totalMs = 0;
long long constructMs = 0;
long long startMs = 0;
long long startupMs = 0;
long long holdMs = 0;
long long stopMs = 0;
};
bool envIsOne(const char* name) {
const char* value = std::getenv(name);
return value && std::string_view(value) == "1";
}
std::optional<spdlog::level::level_enum> parseLogLevel(const std::string_view level) {
if (level == "trace") {
return spdlog::level::trace;
}
if (level == "debug") {
return spdlog::level::debug;
}
if (level == "info") {
return spdlog::level::info;
}
if (level == "warn") {
return spdlog::level::warn;
}
if (level == "error") {
return spdlog::level::err;
}
if (level == "critical") {
return spdlog::level::critical;
}
if (level == "off") {
return spdlog::level::off;
}
return std::nullopt;
}
double meanMs(const std::vector<long long>& values) {
if (values.empty()) {
return 0.0;
}
long long sum = 0;
for (const long long value : values) {
sum += value;
}
return static_cast<double>(sum) / static_cast<double>(values.size());
}
long long percentileMs(const std::vector<long long>& values, int p) {
return yams::bench::percentileNearestRank(values, static_cast<double>(p) / 100.0);
}
} // namespace
static bool isSocketPermissionDenied(const yams::Error& error) {
const std::string_view message{error.message};
return message.find("Operation not permitted") != std::string_view::npos ||
message.find("Permission denied") != std::string_view::npos;
}
int runBenchmark() {
const char* enable = std::getenv("YAMS_ENABLE_DAEMON_BENCH");
if (!enable || std::string_view(enable) != "1") {
std::cout << "Skipping daemon warm latency bench (set YAMS_ENABLE_DAEMON_BENCH=1)\n";
return 0; // treat skip as pass for Meson runner
}
const bool profileLoop = envIsOne("YAMS_DAEMON_BENCH_PROFILE_LOOP");
const bool minThreads = envIsOne("YAMS_DAEMON_BENCH_MIN_THREADS");
const int iterations =
yams::bench::readIntEnv("YAMS_DAEMON_BENCH_ITERATIONS", profileLoop ? 200 : 1, 1, 10000);
const int holdMs =
yams::bench::readIntEnv("YAMS_DAEMON_BENCH_SLEEP_MS", profileLoop ? 0 : 50, 0, 5000);
const int maxTotalMs =
yams::bench::readIntEnv("YAMS_DAEMON_BENCH_MAX_TOTAL_MS", 5000, 100, 300000);
if (const char* ll = std::getenv("YAMS_DAEMON_BENCH_LOG_LEVEL"); ll && *ll) {
if (auto parsedLevel = parseLogLevel(ll); parsedLevel.has_value()) {
spdlog::set_level(*parsedLevel);
}
} else {
spdlog::set_level(spdlog::level::off);
}
std::vector<yams::test::ScopedEnvVar> environment;
if (minThreads) {
if (!yams::config::getenv_nonempty("YAMS_WORK_COORDINATOR_THREADS")) {
environment.emplace_back("YAMS_WORK_COORDINATOR_THREADS", std::string{"1"});
}
if (!yams::config::getenv_nonempty("YAMS_IO_THREADS")) {
environment.emplace_back("YAMS_IO_THREADS", std::string{"1"});
}
}
std::cout << "Daemon bench config: iterations=" << iterations << " hold_ms=" << holdMs
<< " max_total_ms=" << maxTotalMs;
if (profileLoop) {
std::cout << " profile_loop=1";
}
if (minThreads) {
std::cout << " min_threads=1";
}
std::cout << "\n";
environment.emplace_back("YAMS_DB_OPEN_TIMEOUT_MS", std::string{"1000"});
environment.emplace_back("YAMS_DB_MIGRATE_TIMEOUT_MS", std::string{"1500"});
environment.emplace_back("YAMS_SEARCH_BUILD_TIMEOUT_MS", std::string{"1000"});
environment.emplace_back("YAMS_DISABLE_VECTORS", std::string{"1"});
std::vector<TimingSample> samples;
samples.reserve(static_cast<size_t>(iterations));
for (int i = 0; i < iterations; ++i) {
const auto nowTicks =
static_cast<unsigned long>(steady_clock_t::now().time_since_epoch().count() & 0xfffff);
const fs::path runtime_root =
fs::temp_directory_path() / ("ydbench_" + std::to_string(::getpid()) + "_" +
std::to_string(i) + "_" + std::to_string(nowTicks));
yams::daemon::DaemonConfig cfg;
cfg.dataDir = runtime_root / "data";
cfg.socketPath = runtime_root / "sock";
cfg.pidFile = runtime_root / "daemon.pid";
cfg.logFile = runtime_root / "daemon.log";
cfg.enableModelProvider = false;
cfg.autoLoadPlugins = false;
std::error_code se;
fs::create_directories(cfg.dataDir, se);
const auto totalStart = steady_clock_t::now();
auto daemon = std::make_unique<yams::daemon::YamsDaemon>(cfg);
const auto constructEnd = steady_clock_t::now();
const auto startBegin = constructEnd;
auto start = daemon->start();
const auto startEnd = steady_clock_t::now();
if (!start) {
if (isSocketPermissionDenied(start.error())) {
std::cout << "Skipping: UNIX domain sockets not permitted: "
<< start.error().message << "\n";
return 0;
}
std::cerr << "Failed to start daemon: " << start.error().message << "\n";
return 1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(holdMs));
const auto stopBegin = steady_clock_t::now();
daemon->stop();
const auto stopEnd = steady_clock_t::now();
const TimingSample sample{
.totalMs =
std::chrono::duration_cast<std::chrono::milliseconds>(stopEnd - totalStart).count(),
.constructMs =
std::chrono::duration_cast<std::chrono::milliseconds>(constructEnd - totalStart)
.count(),
.startMs = std::chrono::duration_cast<std::chrono::milliseconds>(startEnd - startBegin)
.count(),
.startupMs =
std::chrono::duration_cast<std::chrono::milliseconds>(startEnd - totalStart)
.count(),
.holdMs =
std::chrono::duration_cast<std::chrono::milliseconds>(stopBegin - startEnd).count(),
.stopMs =
std::chrono::duration_cast<std::chrono::milliseconds>(stopEnd - stopBegin).count(),
};
samples.push_back(sample);
if (iterations == 1) {
std::cout << "Warm start/stop: " << sample.totalMs << " ms\n";
} else {
std::cout << "Warm start/stop: " << sample.totalMs << " ms"
<< " (iter " << (i + 1) << "/" << iterations << ")\n";
}
std::cout << "Split ms: construct=" << sample.constructMs << " start=" << sample.startMs
<< " startup=" << sample.startupMs << " hold=" << sample.holdMs
<< " stop=" << sample.stopMs << "\n";
std::error_code ec;
fs::remove_all(runtime_root, ec);
if (sample.totalMs >= maxTotalMs) {
std::cerr << "Warm start/stop exceeded " << maxTotalMs << "ms: " << sample.totalMs
<< " ms\n";
return 2;
}
}
std::vector<long long> totals;
std::vector<long long> constructs;
std::vector<long long> starts;
std::vector<long long> startups;
std::vector<long long> holds;
std::vector<long long> stops;
totals.reserve(samples.size());
constructs.reserve(samples.size());
starts.reserve(samples.size());
startups.reserve(samples.size());
holds.reserve(samples.size());
stops.reserve(samples.size());
for (const TimingSample& sample : samples) {
totals.push_back(sample.totalMs);
constructs.push_back(sample.constructMs);
starts.push_back(sample.startMs);
startups.push_back(sample.startupMs);
holds.push_back(sample.holdMs);
stops.push_back(sample.stopMs);
}
std::cout << "Summary total_ms: mean=" << meanMs(totals) << " p50=" << percentileMs(totals, 50)
<< " p90=" << percentileMs(totals, 90) << " min=" << percentileMs(totals, 0)
<< " max=" << percentileMs(totals, 100) << "\n";
std::cout << "Summary startup_ms: mean=" << meanMs(startups)
<< " p50=" << percentileMs(startups, 50) << " p90=" << percentileMs(startups, 90)
<< "\n";
std::cout << "Summary construct_ms: mean=" << meanMs(constructs)
<< " p50=" << percentileMs(constructs, 50) << " p90=" << percentileMs(constructs, 90)
<< "\n";
std::cout << "Summary start_ms: mean=" << meanMs(starts) << " p50=" << percentileMs(starts, 50)
<< " p90=" << percentileMs(starts, 90) << "\n";
std::cout << "Summary stop_ms: mean=" << meanMs(stops) << " p50=" << percentileMs(stops, 50)
<< " p90=" << percentileMs(stops, 90) << "\n";
return 0;
}
int main() noexcept {
try {
return runBenchmark();
} catch (const std::exception& error) {
std::cerr << "Daemon warm latency benchmark failed: " << error.what() << '\n';
return 1;
} catch (...) {
std::cerr << "Daemon warm latency benchmark failed with an unknown exception\n";
return 1;
}
}