Skip to content

perf(net): receive-path fixes, no-alloc enforcement over the whole frame, latency roadmap - #2

Merged
jdardash merged 12 commits into
mainfrom
perf/hot-path-phase0
Aug 2, 2026
Merged

perf(net): receive-path fixes, no-alloc enforcement over the whole frame, latency roadmap#2
jdardash merged 12 commits into
mainfrom
perf/hot-path-phase0

Conversation

@jdardash

@jdardash jdardash commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Phase 0 of LATENCY-ROADMAP.md, which this PR also adds.

What

  • net::ByteBuffer (new header): a vector whose resize default-initializes. FrameReader and the Schannel backend grew their receive buffers by a 32 KiB chunk per socket read and trimmed back to what arrived; with a plain vector that was 32 KiB of memset per read over bytes recv was about to overwrite.
  • Schannel decrypt path: plaintext now goes straight to the caller's buffer (was: two copies through an intermediate), and the unconsumed tail of a pipelined TLS record is moved in place by the existing take_extra (was: a fresh heap-allocated vector per record on the common path).
  • No-allocation enforcement extended to the Kraken decoder, Feed::handle end to end with the checksum verified, and the frame reader's poll loop. The probe covered the book — 0.3% of the frame cost.
  • CROSSBOOK_NATIVE + release-native preset: opt-in -march=native (/arch:AVX2 on MSVC) plus LTO. Off by default; README numbers stay on plain release.
  • LATENCY-ROADMAP.md: where the gap to professional software trading systems is (environment tail, JSON ceiling, compute already competitive) and the phase order for closing it, with sources.

Verification

  • 341/341 tests pass locally (MSVC Release).
  • The Schannel path has no unit tests, so it was verified live: an 8 s Kraken capture through the new decrypt path, replayed with crossbook_verify — 250 of 250 checksums match, so the decrypted bytes are byte-exact.
  • A sabotage run (wrong checksum in the Feed no-alloc test) fails at the warm-up assertion, proving the guarded loop really runs the verify path.
  • New/touched lines are clang-format-clean locally (20.1.8; CI's 18.1.8 is the authority).

Note: main's CI is currently red on the fuzz smoke and format jobs from an unrelated commit; a fix is already in flight on main. This branch is off 909ae9e and touches neither area.


🤖 Generated with Claude Code

jdardash added 12 commits August 2, 2026 10:39
Four-track research pass: repo hot-path inventory, venue wire formats,
software tick-to-trade practice, tail-latency engineering. Records the
three-way split of the gap (environment tail, JSON ceiling, compute
already competitive) and the phase order for closing it.
…me reader

The probe covered the book — 0.3% of the frame. The claim it existed to
enforce is about the other 99.7%: the decoder, Feed::handle end to end
with the checksum verified, and the transport's poll loop. All three are
allocation-free in steady state; now CI says so instead of the README.

No Catch2 macros run inside the armed regions — an assertion handler may
allocate, and each guarded loop's work is asserted after disarming
(levels seen, checksums verified, messages pumped).
…caller

Two receive paths grew a std::vector<char> by a 32 KiB chunk on every
socket read and trimmed it back to what arrived; vector::resize
value-initializes, so that was 32 KiB of memset per read over bytes the
transport was about to overwrite. net::ByteBuffer is the same vector
with an allocator whose construct() default-initializes — resize
becomes bookkeeping, everything else is unchanged.

The Schannel decrypt path also copied plaintext twice (DecryptMessage's
in-place output into plain_, plain_ into the caller) and built a fresh
vector for the unconsumed tail of every pipelined TLS record — a heap
allocation on the common path, since a busy feed routinely lands the
next record behind the current one in the same segment. Plaintext now
goes straight to the caller's buffer, overflow beyond the caller's ask
is the only thing plain_ holds, and the tail is moved in place by
take_extra, which already existed and already did exactly this.

Verified live: 8 s Kraken capture through the new path, then replayed —
250 of 250 checksums match, so the decrypted bytes are byte-exact.
Opt-in -march=native (/arch:AVX2 on MSVC) plus LTO, for measuring the
ceiling on one's own hardware. Off by default and not inherited by any
other preset: the README's numbers must come from the configuration a
consumer gets, and a binary tuned to the build machine dies with an
illegal instruction on the next one.
GCC's -Wsign-conversion under -Werror rejects promoting the int from
the conditional into a uint64_t accumulator; MSVC and Clang did not
flag it, which is what the GCC floor job exists to catch.
set_read_timeout(0) now means busy-poll — non-blocking socket, reads
return kTimeout immediately — rather than the platforms' block-forever.
crossbook_capture grows --busy-poll and, where the platform can say,
a kernel-to-user delivery histogram: the gap between the kernel
stamping a segment and the loop holding the decoded message, which is
the number busy-poll exists to shrink.

The timestamps were a lesson in what Linux actually offers TCP:
SIOCGSTAMP is unsupported for SOCK_STREAM and SO_TIMESTAMPNS is
silently a no-op there — both learned empirically. The one API that
works is SO_TIMESTAMPING with RX_SOFTWARE, delivered as SCM_TIMESTAMPING
control messages on the recvmsg that carries the data. Which forces an
architectural correction: OpenSSL can no longer own the fd, because a
backend that lets SSL_read call recv() itself can never see a control
message. Reads now flow through TcpSocket via a custom BIO on the
OpenSSL path — the same route Schannel always took — with kTimeout
mapped to the retry flags SSL_read expects.

The transport also gains its first tests: loopback coverage of the
busy-poll contract, the ordinary timed path, and the timestamp, wired
in via a deferred CMake hook because tests/ is configured before
src/net exists.

Verified live on Linux against Kraken over TLS: busy-poll cut
kernel-to-user p50 from 150.4 us to 82.4 us in back-to-back 15 s
captures (WSL2, unpinned; the p99 wants the isolated core the roadmap
pairs it with). Windows: 344/344 tests, live capture clean, timestamps
honestly reported unavailable.
…ointed at

Hand-rolled reader for schema spot_stream 1:0 — depth diff and depth
snapshot to the same DecodedMessage the JSON decoders produce, same
route-before-trust and refuse-rather-than-round contracts, and SBE
blockLengths honoured so a newer minor schema skips cleanly instead of
desyncing. Ten fixture tests hand-encoded from the schema XML, including
a byte-by-byte truncation sweep and a feed-level end-to-end over the
recovery state machine. Live capture is blocked on an Ed25519 API key,
which stream-sbe.binance.com requires even for public data.

Also: roadmap updated with what the phases measured — the WSL2 sweep
(6.3x tail improvement from the OS change alone), the busy-poll live
numbers, and one lever measured dead (unchecked digit accumulation in
parse_fixed: same-state A/B showed noise; reverted, recorded, so nobody
re-optimizes it on vibes). 354/354 tests Windows, 353/353 Linux.
…s its wrap

The loopback test qualified htonl/ntohs with :: — a parse error on
macOS, where both are macros. And the SBE files now carry the exact
formatting clang-format wanted for the lines it flagged. Caught by the
matrix, as designed: MSVC, GCC 13, and clang 14 all accepted both.
@jdardash
jdardash merged commit 65a2ef4 into main Aug 2, 2026
27 checks passed
@jdardash
jdardash deleted the perf/hot-path-phase0 branch August 2, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant