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
5.5 KiB
M7 performance baseline
Measured against docs/04-engine-design.md §8's targets, via tools/bench/vdm_bench
(see that file's header comment for the exact commands — reproduced below with their
actual output). --preset release, this machine, 2026-09-11. This is a baseline
record, not a sign-off: two of the three numbers below don't clear the DoD line yet, and
that's stated plainly rather than rounded away — see "Open gaps".
Commands and results
$ cmake --preset release && cmake --build --preset release
$ bin/vdm_bench throughput --size 5G --require-mbps 940 --max-cpu-pct 8
throughput: 5368709120 bytes in 2.99s
throughput 14371.00 Mbps
cpu 196.52 % of one core
peak RSS 22.81 MiB
Against tools/bench/support/local_server.hpp's busybox loopback server, not a real 1
Gbit link — no such link was available to test against in this environment, so
--require-mbps/--max-cpu-pct weren't meaningfully exercised here (loopback trivially
clears 940 Mbps; the 196% CPU figure reflects driving a link far faster than 1 Gbit, not
the 1-Gbit-saturated cost the target is about). This needs re-running against a real
1 Gbit peer before it can stand as the actual M1/M7 sign-off number.
$ bin/vdm_bench load --tasks 20 --require-rss-kb 61440
load: 20 tasks, 0 failed, 17.06s wall
peak RSS 69.77 MiB
FAIL: peak RSS 71448 KiB > allowed 61440 KiB
Default Config (default_segments=8, max_active_segments=32, default_buffer_bytes=1 MiB), default --task-size 4M. Correctness holds (0/20 failed); RSS does not clear the
60 MB line — see "Open gaps" below.
$ bin/vdm_bench alloc-check --size 512M --window-s 2
alloc-check: 4 allocations in 2.00s (budget 15)
Clears the no-allocation-on-the-hot-path bar (docs/agents/AGENT-CORE.md) comfortably.
This number is after a real fix landed in the same change:
net::HttpClient::Impl::drain_commands was constructing an (always-allocating, in
libstdc++) std::deque on every worker-loop iteration regardless of whether any command
was actually pending — once per curl_multi_poll wake, i.e. on the transfer hot path. Fixed
by checking w.queue.empty() under the lock before touching local at all. Before the
fix this bench reported thousands of allocations/sec under any sustained transfer.
ASan / UBSan / TSan (M1 DoD: "20-task load test... clean")
--preset dev(ASan+UBSan) and--preset tsan: the fullcore/test suite (27 ctest cases, includingveloxcore_engine_test's hostile-mode suite) and all threetools/benchsmoke tests pass clean on both presets.- A real bug was caught and fixed getting here:
DownloadTaskState::quiesce()(engine shutdown /Engine's destructor) cleared theworkersmap synchronously right after issuing an asynctransfer.cancel(), racing the HttpClient worker thread's still-in-flight write callback into a heap-use-after-free on the segment's ring buffer — ASan-caught viaalloc-check, which (by design) drops itsEnginewhile a download is still active. Fixed by havingquiesce()wait for each worker to drain itself through the sameseg_finishedpath every other exit uses, instead of tearing the map down itself. - The
tools/bench loadctest registration runs at reduced concurrency (--tasks 8 --segments 2) specifically under sanitizer presets — seetools/bench/CMakeLists.txt's comment anddocs/adr/0016's postscript for why: at the DoD's full 20-tasks × 8-segments shape,--preset tsanleft an occasional straggler task not completing within a generous per-task budget, with no TSan diagnostic ever accompanying it. Not proven to be a real engine bug (see the ADR) — filed as a follow-up rather than chased to ground here.
Open gaps
- RSS is ~70 MB against a 60 MB target (~10 MB over, ~18%).
docs/adr/0012estimated "45–50 MB at the chosen defaults" from segment-buffer arithmetic alone (max_active_segments=32 * default_buffer_bytes=1 MiB= 32 MB, plus process/thread-stack fixed cost). A minimal single-tiny-task run here measured that fixed cost at ~14.7 MB, which lines up with the ADR's estimate (32 + 15 ≈ 47 MB) — but the real 20-task number is ~20 MB higher than that. Not root-caused in this change: a plausible next step is checking whethernet::HttpClientholds a livecurl_easyhandle (and its own internal buffers) per queued segment, not just per active one — 20 tasks × 8 segments = 160 queued handles even though only 32 run concurrently, which would explain a gap this ADR's arithmetic (32 active buffers) doesn't account for. - Throughput/CPU numbers are loopback-only. No 1 Gbit link was available to test
against; re-run
throughput --size 5G --require-mbps 940 --max-cpu-pct 8against a real one before treating this as signed off. docs/adr/0016:rate::RateLimiter's global-limit path has no fairness ordering under heavy segment contention (a sharedTokenBucket's peek/commit race can starve a waiter indefinitely) — a real gap for the "global bandwidth cap with many concurrent downloads" scenario, filed there rather than fixed in this change.- The TSan-only load-test straggler noted above (
docs/adr/0016's postscript) — not root-caused; needs reproducing outside a shared/virtualized sandbox to tell "TSan is just slow here" apart from a real timing-sensitive bug (a plausible candidate named in the ADR:CURLOPT_LOW_SPEED_TIMEfalse-tripping under TSan's slowdown).