From 8b464ad877e74256bc858456a503a999ec99baef Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Wed, 25 Mar 2026 12:23:53 +1100 Subject: [PATCH] Fix high CPU usage on Linux due to epoll busy-loop 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 --- FlyingSocks/Sources/AsyncSocket.swift | 2 +- FlyingSocks/Sources/SocketPool+ePoll.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/FlyingSocks/Sources/AsyncSocket.swift b/FlyingSocks/Sources/AsyncSocket.swift index 8d235e96..31f9f009 100644 --- a/FlyingSocks/Sources/AsyncSocket.swift +++ b/FlyingSocks/Sources/AsyncSocket.swift @@ -131,7 +131,7 @@ public struct AsyncSocket: Sendable { @Sendable public func accept() async throws -> AsyncSocket { - try await pool.loopUntilReady(for: .connection, on: socket) { + try await pool.loopUntilReady(for: .read, on: socket) { let file = try socket.accept().file let socket = Socket(file: file) return try AsyncSocket(socket: socket, pool: pool) diff --git a/FlyingSocks/Sources/SocketPool+ePoll.swift b/FlyingSocks/Sources/SocketPool+ePoll.swift index a104e019..66ef808e 100644 --- a/FlyingSocks/Sources/SocketPool+ePoll.swift +++ b/FlyingSocks/Sources/SocketPool+ePoll.swift @@ -105,6 +105,7 @@ public struct ePoll: EventQueue { } mutating func setEvents(_ events: Socket.Events, for socket: Socket.FileDescriptor) throws { + if existing[socket] == events { return } var event = CSystemLinux.epoll_event() event.events = events.epollEvents.rawValue event.data.fd = socket.rawValue @@ -242,7 +243,9 @@ private struct EPOLLEvents: OptionSet, Hashable { private extension Socket.Events { var epollEvents: EPOLLEvents { - reduce(EPOLLEvents()) { [$0, $1.epollEvent] } + var events = reduce(EPOLLEvents()) { [$0, $1.epollEvent] } + events.insert(.edgeTriggered) + return events } static func make(from pollevents: EPOLLEvents) -> Socket.Events {