Three subcommands in one binary, driving vdm::Engine directly (docs/04 §8): - throughput: a single download against a fast local origin (support/local_server.hpp, busybox httpd), reporting Mbps/CPU%/RSS. Gates on --require-mbps/--max-cpu-pct only when passed, so the ctest smoke registration stays a correctness check, not a hardware-dependent perf gate -- the real 1-Gbit-link sign-off is a manual/CI job (see the file's header comment). - load: N concurrent tasks against tools/testserver's `throttled` mode (support/testserver_client.hpp), reporting peak RSS via getrusage(). Paced externally rather than through the engine's own rate::RateLimiter or busybox: the limiter's pause/resume path allocates on every throttle event (would contaminate alloc-check's measurement) and under heavy segment contention was found to starve individual tasks indefinitely (see docs/adr/0016, added here); busybox couldn't sustain the DoD's ~160 concurrent connections (20 tasks * default_segments=8) reliably. The ctest registration runs at reduced concurrency under sanitizer presets -- see the CMakeLists.txt comment and the ADR's postscript. - alloc-check: operator new/delete overridden process-wide, sampling the allocation count across a steady mid-transfer window against a paced tools/testserver origin. Caught a real bug in the same change (see the http_client.cpp commit) and, by dropping its Engine mid-download to end cleanly, also surfaced the quiesce() use-after-free (see that commit). core/docs/m7-baseline.md records actual measured numbers against the M1/M7 DoD lines, including where they don't clear yet (RSS ~70 MB vs a 60 MB target; throughput/CPU only measured on loopback, no 1 Gbit link available here) rather than rounding them away. docs/adr/0016 documents a rate::RateLimiter fairness gap found building the load subcommand: a single shared TokenBucket under heavy segment contention has no fairness ordering across its peek/commit race and can starve a waiter well past what its configured rate implies. Filed as a follow-up (it's a core/src/rate design question, not a tools/bench one) rather than fixed here, along with a related TSan-only load-test straggler that could not be root-caused in this environment. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Q3QrF7rCt21bkAjt9BCDFQ
117 lines
4.4 KiB
C++
117 lines
4.4 KiB
C++
// tools/bench/support/local_server.hpp — spawn a fast static-file HTTP server for the
|
|
// throughput/load benches.
|
|
//
|
|
// tools/testserver's Python server is right for the hostile-mode correctness suite, but
|
|
// its body generation hashes every chunk in Python, which bottlenecks well below anything
|
|
// resembling a saturated link — a throughput number measured against it would be measuring
|
|
// the test server, not the engine. `busybox httpd` is a small C static file server that
|
|
// supports Range/If-Range/ETag properly (verified: 206 + Content-Range + ETag +
|
|
// Last-Modified + Accept-Ranges on a ranged GET) and comes with coreutils on most Linux
|
|
// boxes, so it stands in for "a plain, honest, reasonably fast origin" without pulling in
|
|
// nginx or writing our own.
|
|
//
|
|
// Linux-only (fork/exec/waitpid), like tools/testserver's fixture. If busybox is missing
|
|
// or every candidate port is taken, available() is false and the caller should skip.
|
|
|
|
#ifndef VDM_BENCH_LOCAL_SERVER_HPP
|
|
#define VDM_BENCH_LOCAL_SERVER_HPP
|
|
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
namespace vdm::bench {
|
|
|
|
class LocalServer {
|
|
public:
|
|
// Serves `docroot` over HTTP on 127.0.0.1. Tries a handful of pseudo-random high ports
|
|
// (busybox exits(1) with "bind: Address already in use" on a taken one; there's no
|
|
// ephemeral-port mode to ask it for one back, unlike tools/testserver's Python server).
|
|
explicit LocalServer(std::string docroot) : docroot_(std::move(docroot)) {
|
|
unsigned seed = static_cast<unsigned>(::getpid()) ^
|
|
static_cast<unsigned>(std::chrono::steady_clock::now()
|
|
.time_since_epoch()
|
|
.count());
|
|
std::srand(seed);
|
|
for (int attempt = 0; attempt < 8 && pid_ < 0; ++attempt) {
|
|
int candidate = 20000 + (std::rand() % 40000);
|
|
if (try_start(candidate))
|
|
port_ = candidate;
|
|
}
|
|
}
|
|
|
|
~LocalServer() {
|
|
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);
|
|
}
|
|
}
|
|
|
|
LocalServer(const LocalServer &) = delete;
|
|
LocalServer &operator=(const LocalServer &) = delete;
|
|
|
|
[[nodiscard]] bool available() const { return pid_ > 0; }
|
|
[[nodiscard]] std::string url(const std::string &path) const {
|
|
return "http://127.0.0.1:" + std::to_string(port_) + path;
|
|
}
|
|
|
|
private:
|
|
bool try_start(int port) {
|
|
pid_t pid = ::fork();
|
|
if (pid < 0)
|
|
return false;
|
|
if (pid == 0) {
|
|
int devnull = ::open("/dev/null", O_WRONLY);
|
|
if (devnull >= 0) {
|
|
::dup2(devnull, STDOUT_FILENO);
|
|
::dup2(devnull, STDERR_FILENO);
|
|
}
|
|
::execlp("busybox", "busybox", "httpd", "-f", "-p", std::to_string(port).c_str(),
|
|
"-h", docroot_.c_str(), static_cast<char *>(nullptr));
|
|
::_exit(127); // busybox not found
|
|
}
|
|
// Give it a moment to either bind-and-block (success) or bind-fail-and-exit.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
|
int status = 0;
|
|
pid_t r = ::waitpid(pid, &status, WNOHANG);
|
|
if (r == pid)
|
|
return false; // already exited: port taken, or busybox missing
|
|
pid_ = pid;
|
|
return true;
|
|
}
|
|
|
|
std::string docroot_;
|
|
pid_t pid_ = -1;
|
|
int port_ = 0;
|
|
};
|
|
|
|
// Create (or truncate to) a file of `bytes` length without writing them: content is
|
|
// whatever the filesystem hands back for a hole (zeros), which is fine for a throughput
|
|
// measurement — we're timing the transfer and write path, not verifying content.
|
|
[[nodiscard]] inline bool make_sparse_file(const std::string &path, std::uint64_t bytes) {
|
|
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
if (fd < 0)
|
|
return false;
|
|
bool ok = ::ftruncate(fd, static_cast<off_t>(bytes)) == 0;
|
|
::close(fd);
|
|
return ok;
|
|
}
|
|
|
|
} // namespace vdm::bench
|
|
|
|
#endif // VDM_BENCH_LOCAL_SERVER_HPP
|