Skip to content

Commit d8fa4a2

Browse files
authored
Reduce idle CPU usage in WebSocket monitor loop (#3762)
Replace short-interval idle polling in the Socket.IO WebSocket receive loop with socket-readiness waiting. Previously, an idle WebSocket connection repeatedly called curl_easy_recv(), received CURLE_AGAIN, slept for 20ms, and retried. This caused approximately 50 idle wakeups per second while --monitor was otherwise idle. The receive loop now waits on the active libcurl socket before retrying, reducing idle CPU usage and context-switch churn while preserving WebSocket notification and ping/pong behaviour. Validation on Fedora laptop: - pidstat idle CPU reduced from ~0.82% to ~0.05% - perf task-clock reduced from ~1466 ms / 147 s to ~42 ms / 129 s - context switches reduced from ~50/sec to ~2/sec - strace confirms the 20ms recvfrom(EAGAIN) sleep loop is gone - local upload, WebSocket notification receipt, and /delta processing remain functional
1 parent 32a0110 commit d8fa4a2

2 files changed

Lines changed: 100 additions & 4 deletions

File tree

src/curlWebsockets.d

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import etc.c.curl : CURL, CURLcode, curl_easy_cleanup, curl_easy_getinfo,
1010
curl_easy_init, curl_easy_perform, curl_easy_recv, curl_easy_reset,
1111
curl_easy_send, curl_easy_setopt;
1212

13+
import core.stdc.errno;
1314
import core.stdc.string : memcpy, memmove;
15+
import core.sys.posix.poll;
1416
import core.time : MonoTime, dur;
1517
import std.array : Appender, appender;
1618
import std.base64 : Base64;
@@ -60,6 +62,11 @@ private:
6062
enum int CURLOPT_SSL_ENABLE_ALPN = 226; // CURLOPT_SSL_ENABLE_ALPN
6163
enum int CURLOPT_SSL_ENABLE_NPN = 225; // CURLOPT_SSL_ENABLE_NPN
6264

65+
// libcurl info / result constants used by CONNECT_ONLY receive handling
66+
// CURLINFO_ACTIVESOCKET = CURLINFO_SOCKET + 44 = 0x500000 + 44
67+
enum int CURLINFO_ACTIVESOCKET = 0x50002C;
68+
enum int CURLE_AGAIN_CODE = 81;
69+
6370
// HTTP version flags (for CURLOPT_HTTP_VERSION)
6471
enum long CURL_HTTP_VERSION_NONE = 0;
6572
enum long CURL_HTTP_VERSION_1_0 = 1;
@@ -193,6 +200,15 @@ public:
193200
logCurlWebsocketOutput("Timeout waiting for HTTP upgrade response");
194201
return -6;
195202
}
203+
204+
// Avoid a tight curl_easy_recv(CURLE_AGAIN) loop while waiting
205+
// for the server's HTTP 101 upgrade response. The WebSocket
206+
// worker is not yet marked connected here, but the active
207+
// CONNECT_ONLY socket is already available from libcurl.
208+
if (waitReadable(250) < 0) {
209+
logCurlWebsocketOutput("Failed waiting for HTTP upgrade response readability");
210+
return -5;
211+
}
196212
continue;
197213
}
198214
hdrs ~= cast(const(char)[]) tmp[0 .. cast(size_t)got];
@@ -309,6 +325,43 @@ public:
309325
}
310326
}
311327

328+
// Wait until the active libcurl CONNECT_ONLY socket is readable.
329+
// Returns 1 when readable, 0 on timeout or EINTR, and -1 on socket/error.
330+
int waitReadable(int timeoutMs) {
331+
if (curl is null) return -1;
332+
333+
int activeSocket = -1;
334+
auto infoRc = curl_easy_getinfo(curl, cast(int)CURLINFO_ACTIVESOCKET, &activeSocket);
335+
if (infoRc != 0 || activeSocket < 0) {
336+
logCurlWebsocketOutput("Unable to obtain active WebSocket socket from libcurl");
337+
return -1;
338+
}
339+
340+
if (timeoutMs < 0) timeoutMs = 0;
341+
342+
pollfd fds;
343+
fds.fd = activeSocket;
344+
fds.events = POLLIN;
345+
fds.revents = 0;
346+
347+
int pollRc = poll(&fds, 1, timeoutMs);
348+
if (pollRc == 0) return 0;
349+
if (pollRc < 0) {
350+
if (errno == EINTR) return 0;
351+
logCurlWebsocketOutput("poll() failed while waiting for WebSocket readability");
352+
return -1;
353+
}
354+
355+
if ((fds.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
356+
logCurlWebsocketOutput("WebSocket socket reported poll error/hangup/invalid state");
357+
websocketConnected = false;
358+
return -1;
359+
}
360+
361+
if ((fds.revents & POLLIN) != 0) return 1;
362+
return 0;
363+
}
364+
312365
private:
313366
struct ParsedUrl {
314367
bool ok;
@@ -420,7 +473,16 @@ private:
420473
int recvSome(ubyte[] buf) {
421474
size_t got = 0;
422475
auto rc = curl_easy_recv(curl, cast(void*)buf.ptr, buf.length, &got);
423-
if (rc != 0) return 0; // treat EAGAIN etc. as "no bytes now"
476+
if (rc != 0) {
477+
if (cast(int)rc == CURLE_AGAIN_CODE) {
478+
return 0; // socket would block; caller should wait for readability
479+
}
480+
481+
websocketConnected = false;
482+
logCurlWebsocketOutput("curl_easy_recv failed while receiving WebSocket data");
483+
return -1;
484+
}
485+
424486
return cast(int)got;
425487
}
426488

src/socketio.d

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ private:
138138
return atomicLoad(self.pleaseStop);
139139
}
140140

141+
// Idle WebSocket receive waits are deliberately bounded so shutdown,
142+
// subscription expiry checks, and notification URL renewal checks remain
143+
// responsive while avoiding the previous 20ms idle polling loop.
144+
enum int websocketIdleReadWaitMs = 1000;
145+
enum int websocketHandshakeReadWaitMs = 250;
146+
147+
static int waitForWebSocketReadable(OneDriveSocketIo self, curlWebsockets.CurlWebSocket ws, int timeoutMs) {
148+
if (stopRequested(self)) return -2;
149+
auto waitRc = ws.waitReadable(timeoutMs);
150+
if (stopRequested(self)) return -2;
151+
return waitRc;
152+
}
153+
141154
static bool interruptibleSleep(OneDriveSocketIo self, long totalMs, int stepMs = 100) {
142155
long sleptMs = 0;
143156
while (sleptMs < totalMs) {
@@ -365,10 +378,21 @@ private:
365378
lastPingAt = Clock.currTime(UTC());
366379
}
367380

368-
// Receive message
381+
// Receive message. When the WebSocket is idle, wait on the
382+
// active socket becoming readable instead of polling every 20ms.
369383
auto msg = self.ws.recvText();
370384
if (msg.length == 0) {
371-
if (!interruptibleSleep(self, 20, 20)) return;
385+
if (!self.ws.isConnected()) {
386+
logSocketIOOutput("WebSocket disconnected while receiving; restarting WebSocket");
387+
break;
388+
}
389+
390+
auto waitRc = waitForWebSocketReadable(self, self.ws, websocketIdleReadWaitMs);
391+
if (waitRc == -2) return;
392+
if (waitRc < 0) {
393+
logSocketIOOutput("WebSocket readability wait failed; restarting WebSocket");
394+
break;
395+
}
372396
continue;
373397
}
374398

@@ -495,7 +519,17 @@ private:
495519

496520
auto msg = ws.recvText();
497521
if (msg.length == 0) {
498-
if (!interruptibleSleep(self, 25, 25)) return false;
522+
if (!ws.isConnected()) {
523+
logSocketIOOutput("WebSocket disconnected during Socket.IO open handshake");
524+
return false;
525+
}
526+
527+
auto waitRc = waitForWebSocketReadable(self, ws, websocketHandshakeReadWaitMs);
528+
if (waitRc == -2) return false;
529+
if (waitRc < 0) {
530+
logSocketIOOutput("WebSocket readability wait failed during Socket.IO open handshake");
531+
return false;
532+
}
499533
continue;
500534
}
501535

0 commit comments

Comments
 (0)