perf(server): stop idle-client CPU spin (arm EPOLLOUT on demand)#3
Merged
Conversation
Client fds were registered with a permanent EPOLLOUT. Since a TCP socket is almost always writable and epoll is level-triggered, epoll_wait() returned on every tick even with no traffic, spinning the event loop at ~100% CPU per connected idle client. Register clients with EPOLLIN only and reconcile each client's write-interest once per loop iteration: EPOLLOUT is armed only while output is queued and dropped once the send buffer drains. epoll_ctl is issued only when the interest actually changes. Measured (1 idle client, no traffic): 99.3% -> 0.0% CPU. Verified registration replies and channel broadcasts still flush.
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.
Closes #2
Problem
Each accepted client was registered with
EPOLLIN | EPOLLOUTpermanently(
Server::acceptClient). Because epoll is level-triggered and a TCP socket isalmost always writable,
epoll_wait()returned on every loop iteration evenwith no traffic, so
Server::run()spun at ~100% CPU for each connected idleclient. The existing
modifyEpoll()helper was defined but never called.Approach
Reconcile write-interest once per loop iteration (chosen over instrumenting
every enqueue site):
EPOLLINonly.and set each one's interest to
EPOLLIN | (hasPendingData() ? EPOLLOUT : 0),issuing
epoll_ctlonly when the interest actually changed (_epollMask).Why this and not "arm EPOLLOUT at each
queueMessagecall site": output isqueued from many places (command handlers, channel broadcasts into other
clients' buffers, PING in
checkTimeouts, extensions). The sweep covers all ofthem without touching any call site, so there is no path that can be forgotten
and leave a client's output stuck. Cost is an O(n) walk over a bounded client
set per iteration, syscall-only-on-change. Delivery latency is unchanged (both
designs flush on the next
epoll_wait).Changes
src/Server.cpp,include/Server.hpp: the fix (registerEPOLLINonly;_epollMask;updateEpollInterest(); the reconcile sweep inrun(); maskcleanup in
disconnectClient).tests/test_eventloop.cpp(+tests/Makefile): regression test — countsevent-loop iterations via the
IServerExtension::onTickseam (zeroproduction code added).
CLAUDE.md: corrected a stale note; added the workflow-artifacts policy..gitignore: trailing-newline fix (separate housekeeping commit).Verification
EventLoopTest.IdleClientDoesNotSpinEventLoopmake mandatoryandmake(full),-Wall -Wextra -Werror -std=c++98.The regression test discriminates: it fails on the unfixed tree and passes on
the fixed tree (numbers above).
Process
Followed the audit → plan → adversarial-review workflow. The reviewer returned
APPROVE WITH CHANGES; all points addressed (regression test added, the
.gitignorechange split into its own commit, raw verification captured). Theworkflow documents (
01-audit.md…04-review.md) are kept out of git per theartifacts policy; their findings are distilled above. I can paste the review
verdict as a comment if useful.
What I'd like you to validate
src/Server.cppandinclude/Server.hpp; everything else is the test, docs, and thehousekeeping commit.