Repository navigation
perf(reactor): RX-epoll receive path, no epoll_ctl per keep-alive request - #371
Merged
Merged
Conversation
…uest On the epoll reactor (no io_uring ring, e.g. under a container seccomp profile that blocks io_uring_setup) every keep-alive request paid four syscalls: recv, send, and two epoll_ctl for the one-shot re-arm, because cwist_reactor_add always tried EPOLL_CTL_ADD first and only switched to MOD after it failed with EEXIST. The io_uring reactor pays one send per request, since the re-arm rides the SQE batch. - Serve the RX contract of reactor_rx.h on epoll too. A connection's fd stays registered edge-triggered (no EPOLLONESHOT, user_data fd << 1 | 1), and on readiness the reactor recv()s into the armed buffer and reports through the same completion handler, so http.c needs no change. A full buffer, or an edge that lands while no receive is armed, queues the fd for a receive before the next wait, so no bytes are stranded by the edge trigger. Arms are taken on the run thread only; elsewhere the existing POLL fallback re-arms from the run thread. Any cwist_reactor_add/_mod/_del takes the fd back to the one-shot path. - Keep a process-wide per-fd registration hint so one-shot re-arms go straight to EPOLL_CTL_MOD. The accept path resets it for fresh fds (cwist_reactor_fd_forget). - CWIST_RX_EPOLL=0 restores the one-shot path. A steady keep-alive request on epoll now costs recv + send. Locally (5600X, 4 workers on 2 physical cores, wrk -t8 on the rest): +8-9% req/s and -9-10% P50 on forced epoll, close to the io_uring reactor; io_uring is unchanged. tests/test_rx_epoll.c covers the keep-alive loop with no epoll_ctl, the small-buffer drain without new edges, bytes arriving while unarmed, the hand-off to and back from the one-shot path, a new connection on the same fd, EOF, and off-thread arms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When cwist can't get an io_uring ring, it falls back to epoll (
CWIST_REACTOR_BACKEND=epoll, or a container whose seccomp profile blocksio_uring_setup). Docker's default profile does this, which matters for the the-benchmarker comparison in #319. On the epoll path, each keep-alive request cost four syscalls:sendmsgrecvfromepoll_ctlio_uring_enter/epoll_wait)Two problems on the old path:
EPOLL_CTL_ADDfirst, so every keep-alive request failed oneepoll_ctlbefore theMOD.EPOLLONESHOTneeds aMODon every request anyway.What
reactor_rx.h's receive contract (cwist_reactor_recv_armplus the completion handler) is now served on epoll as well.EPOLLIN|EPOLLRDHUP|EPOLLET, noEPOLLONESHOT,user_data = fd << 1 | 1.recv()s into the armed buffer and calls the samerx_cb.http.cis unchanged.epoll_waitgets a zero timeout).cwist_reactor_add/_mod/_delof the fd takes it back to the one-shot path, and the next RX arm registers it again. Both MOD and ADD report an already-readable fd, so no edge is lost when switching.-EAGAINis never reported: the receive stays armed until bytes, EOF or an error arrive.EPOLL_CTL_MOD.cwist_reactor_fd_forget).CWIST_RX_EPOLL=0restores the old one-shot path. It's documented indocs/runtime-config.md(plus the ko mirror).http.hstruct. The layout is unchanged andcheck_api_baselinepasses.Numbers
Contract app from
benchmarks/web-frameworks(GET /),CWIST_WORKERS=4, server on 2 physical cores (cpus 0,1,6,7),wrk -t8on the other 4, Ryzen 5600X. Each value is the median of 3 interleaved rounds:A second, noisier pass (4 rounds, with other load on the desktop) showed the same relative change: epoll +7% / +9% / +8% req/s at 64 / 256 / 512 conns, with P50 down 10% / 9% / 9%. io_uring stayed within noise.
So epoll is now within a few percent of io_uring at 512 conns, and ahead of it at 64.
Tests
tests/test_rx_epoll.c(wired intoTEST_TARGETS, runs under TSan). It includesreactor.cand countsepoll_ctlcalls, liketest_reactor_wake.cdoes. It covers:epoll_ctlafter the first armcwist_reactor_addtakes the fd over, and RX registers it again afterwardscwist_reactor_fd_forgetmake test: passes in full both withCWIST_REACTOR_BACKEND=epolland on the default backend.test_rx_epoll: clean under TSan and under ASan/UBSan.check_test_wiring,check_env_knobs,check_api_baselineandcwist audit --gateall pass.Refs #319.