core: add tools/bench (throughput/load/alloc-check) and record the M7 baseline

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
This commit is contained in:
2026-09-11 13:12:57 +04:00
co-authored by Claude Sonnet 5
parent 6163898c14
commit b60d4e6f5b
7 changed files with 912 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
// 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