// tools/bench/support/testserver_client.hpp — spawn tools/testserver for benches that need // a genuinely paced, external source (alloc-check's steady-state sampling window). // // LocalServer (busybox httpd) is right for the throughput/load numbers -- it's fast and // honest, closer to "a real origin". But that speed is exactly wrong for alloc-check: at // this server's loopback throughput a modest file transfers in well under the sampling // window, leaving nothing to sample mid-transfer. Rather than pace the transfer with the // engine's own rate::RateLimiter (whose curl-write-callback pause/resume path allocates a // timer node + std::function per throttle event -- exactly the kind of cost alloc-check // exists to catch, so using it to build the fixture would contaminate the measurement), // pace it *externally*: tools/testserver's `throttled` mode sleeps between chunks // server-side, so curl's write callback fires already spaced out and the engine never // takes the pause/resume path at all. // // Linux-only (fork/exec/pipe/kill), same shape as core/tests/net/testserver_fixture.hpp // (which this mirrors rather than includes -- that header lives under core/tests and pulls // in VDM_TESTSERVER_PY via core/tests/CMakeLists.txt's own injection; tools/bench gets its // own so the two test trees stay independently buildable). #ifndef VDM_BENCH_TESTSERVER_CLIENT_HPP #define VDM_BENCH_TESTSERVER_CLIENT_HPP #include #include #include #include #include #include #include #include #include #ifndef VDM_TESTSERVER_PY #define VDM_TESTSERVER_PY "" #endif namespace vdm::bench { class TestServerProc { public: // `throttle_bps` sets the `throttled` mode's rate (testserver's own default is 1 // MiB/s, generally too slow to keep a ctest's TIMEOUT happy at a size big enough to // span a sampling window -- callers pacing a specific --size/--window-s pair should // pass one sized to match, same arithmetic cmd_load uses for its own rate limit). explicit TestServerProc(std::uint64_t throttle_bps = 0) { const char *script = VDM_TESTSERVER_PY; if (!script || !*script || ::access(script, R_OK) != 0) return; int pipefd[2]; if (::pipe(pipefd) != 0) return; pid_ = ::fork(); if (pid_ < 0) { ::close(pipefd[0]); ::close(pipefd[1]); pid_ = -1; return; } if (pid_ == 0) { ::dup2(pipefd[1], STDOUT_FILENO); ::close(pipefd[0]); ::close(pipefd[1]); int devnull = ::open("/dev/null", O_WRONLY); if (devnull >= 0) ::dup2(devnull, STDERR_FILENO); std::string bps_str = std::to_string(throttle_bps ? throttle_bps : 1048576ull); ::execlp("python3", "python3", script, "--port", "0", "--seed", "7", "--throttle-bps", bps_str.c_str(), static_cast(nullptr)); ::_exit(127); } ::close(pipefd[1]); std::string line; char c = 0; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (std::chrono::steady_clock::now() < deadline) { ssize_t r = ::read(pipefd[0], &c, 1); if (r == 1) { if (c == '\n') break; line += c; } else if (r == 0) { break; } else if (errno != EINTR) { break; } } ::close(pipefd[0]); if (!line.empty()) port_ = std::atoi(line.c_str()); std::this_thread::sleep_for(std::chrono::milliseconds(150)); } ~TestServerProc() { if (pid_ > 0) { ::kill(pid_, SIGTERM); int status = 0; for (int i = 0; i < 50; ++i) { if (::waitpid(pid_, &status, WNOHANG) == pid_) return; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } ::kill(pid_, SIGKILL); ::waitpid(pid_, &status, 0); } } TestServerProc(const TestServerProc &) = delete; TestServerProc &operator=(const TestServerProc &) = delete; [[nodiscard]] bool available() const { return port_ > 0; } [[nodiscard]] std::string url(const std::string &path) const { return "http://127.0.0.1:" + std::to_string(port_) + path; } private: pid_t pid_ = -1; int port_ = 0; }; } // namespace vdm::bench #endif // VDM_BENCH_TESTSERVER_CLIENT_HPP