Skip to content

Fix high CPU usage on Linux due to epoll busy-loop#191

Merged
swhitty merged 1 commit intoswhitty:mainfrom
PADL:lhoward/186v2
Apr 18, 2026
Merged

Fix high CPU usage on Linux due to epoll busy-loop#191
swhitty merged 1 commit intoswhitty:mainfrom
PADL:lhoward/186v2

Conversation

@lhoward
Copy link
Copy Markdown
Contributor

@lhoward lhoward commented Mar 25, 2026

NB: these fixes were generated by AmpCode. They appear to fix the issue and also do not break tests.

Three fixes for 100% CPU spinning when HTTPServer is idle on Linux:

  1. Use edge-triggered (EPOLLET) mode for all sockets, not just the canary eventfd. Level-triggered epoll causes epoll_wait() to return immediately when the listening socket is readable, creating a busy loop.

  2. Change accept() to wait on .read events only instead of .connection ([.read, .write]). A listening socket is always writable (EPOLLOUT), so registering for write events causes immediate spurious wakeups. accept() only needs readability (a pending connection).

  3. Skip redundant epoll_ctl(EPOLL_CTL_MOD) calls when the event mask hasn't changed. EPOLL_CTL_MOD re-arms edge-triggered events and can cause spurious wakeups when the condition is already met.

Fixes: #186

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.40%. Comparing base (a8358d1) to head (20e38fc).
⚠️ Report is 17 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #191      +/-   ##
==========================================
- Coverage   92.80%   92.40%   -0.40%     
==========================================
  Files          68       69       +1     
  Lines        3448     3543      +95     
==========================================
+ Hits         3200     3274      +74     
- Misses        248      269      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@lhoward
Copy link
Copy Markdown
Contributor Author

lhoward commented Apr 17, 2026

@swhitty just pinging on this :)

Comment thread FlyingSocks/Sources/AsyncSocket.swift Outdated
@Sendable
public func accept() async throws -> AsyncSocket {
try await pool.loopUntilReady(for: .connection, on: socket) {
try await pool.loopUntilReady(for: .read, on: socket) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we localise this change to just linux for now — I remember I needed write events for iOS disconnection detection so I'll need to look into that

#if canImport(Glibc)
static let connection: Self = [.read]
#else
static let connection: Self = [.read, .write]
#endif

}

mutating func setEvents(_ events: Socket.Events, for socket: Socket.FileDescriptor) throws {
if existing[socket] == events { return }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if existing[socket] == events { return }
guard existing[socket] != events else { return }

var epollEvents: EPOLLEvents {
reduce(EPOLLEvents()) { [$0, $1.epollEvent] }
var events = reduce(EPOLLEvents()) { [$0, $1.epollEvent] }
events.insert(.edgeTriggered)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to pass this as an option to structure ePoll so users can revert to level triggered events if they need to?

let pool = SocketPool<ePoll>(triggering: .edge, maxEvents: ....)
let socket = AsyncSocket(pool: pool)

I don't mind if the default changes to .edge, just a way to easily go back if there is an issue

@swhitty
Copy link
Copy Markdown
Owner

swhitty commented Apr 17, 2026

Thanks for this Luke, just a couple of changes to provide a way to revert the changes at runtime if we need

@swhitty
Copy link
Copy Markdown
Owner

swhitty commented Apr 17, 2026

also, is there an easy way I can reproduce this issue on Linux with FlyingFoxCLI?

I've tried in docker / container and it isn't looping when waiting for connections, do I need to use a VM?

@lhoward
Copy link
Copy Markdown
Contributor Author

lhoward commented Apr 17, 2026

also, is there an easy way I can reproduce this issue on Linux with FlyingFoxCLI?

I've tried in docker / container and it isn't looping when waiting for connections, do I need to use a VM?

I was only able to reproduce it with my code but, I'll try get Claude or Amp to make a repro case.

@lhoward lhoward force-pushed the lhoward/186v2 branch 2 times, most recently from 6a0cf67 to b81b2f3 Compare April 17, 2026 23:57
Three fixes for 100% CPU spinning when HTTPServer is idle on Linux:

1. Use edge-triggered (EPOLLET) mode for all sockets, not just the
   canary eventfd. Level-triggered epoll causes epoll_wait() to return
   immediately when the listening socket is readable, creating a busy
   loop.

2. Change accept() to wait on .read events only instead of .connection
   ([.read, .write]). A listening socket is always writable (EPOLLOUT),
   so registering for write events causes immediate spurious wakeups.
   accept() only needs readability (a pending connection).

3. Skip redundant epoll_ctl(EPOLL_CTL_MOD) calls when the event mask
   hasn't changed. EPOLL_CTL_MOD re-arms edge-triggered events and can
   cause spurious wakeups when the condition is already met.

Fixes: swhitty#186
@swhitty swhitty merged commit c87198d into swhitty:main Apr 18, 2026
12 of 13 checks passed
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.

high CPU usage in FlyingSocks

2 participants