io_wait: run the fd-map consistency check only with EXTRA_DEBUG - #4221
Conversation
|
Hi @Lt-Flash - indeed, the check may be costly, I agree. But it was added while trying to debug some reports related to the TCP reactor. So I see some need for it, without going thru the pain of re-compiling (as we had in the past with the memory allocators) - so maybe we should do this as core param option or so ? something you can on/off without re-compiling. |
|
Hi Bogdan, thank you for taking the time to look at this, and for the context — I hadn't appreciated that the check was born out of real TCP reactor reports, which changes the picture. You're right that something you can switch on without recompiling is worth far more than a compile-time flag, and the allocator history makes the point well. I'm happy to rework the PR along those lines. If I may float a suggestion for the shape, I'd lean towards mirroring what If you'd rather keep it simpler, a straightforward boolean For reference, the numbers that prompted the PR in the first place: about 0.6 ms per call on a 21k-entry reactor, and roughly 13.6% of host CPU with a module doing ~1,500 async operations per second, with UDP drops under load that go away once the walk is off. Whichever you prefer, I'll update the PR (code plus the Option 1 —
|
|
I don't think Option 1 is usable - in order to enable the check you need to enable full debug logging, which will simply flood your logging system. Not to mention that enabling a consistency check by changing a log level is not quite logic / intuitive :P . Now, there is also the issue of having this new core param when comes to the backports... ideally a backport should not introduce new parameters. But please update the PR with Option 2, with "REACTOR_DBG_FD_CHECK" (we need to underline that is a debugging option here) for the devel / 4.1 branch. And in the meanwhile we can see what to do for with the backports |
|
On a second thought, we can backport the new core param (as it is optional, for corner cases) and have it default OFF |
check_io_data() walks every one of the reactor's max_fd_no entries on every io_watch_add() and io_watch_del() - O(open_files_limit) per async registration or removal. Measured on a node with a 21k-entry reactor: ~0.6 ms per call, and 13.6% of host CPU with a module doing ~1,500 async operations per second, with UDP drops under load that disappear once the walk is off. The check exists to debug reactor / fd-map reports, so it stays - but as a runtime debugging option instead of a compile-time one: the new core parameter REACTOR_DBG_FD_CHECK (boolean, default off; on by default in EXTRA_DEBUG builds, which keeps their behaviour unchanged) gates the walk at the top of the macro. When off the cost is one global load and a branch; when on the check is exactly what it was, catching the io_watch_add/del that breaks the map at the moment it happens. Documented in docs/manual/Script-CoreParameters.md as a debugging-only setting.
a93af51 to
3dadd80
Compare
|
Thanks Bogdan — agreed, the log-level shape would be neither intuitive nor practical. PR updated with option 2: a boolean core parameter Happy to prepare the backport PRs with the parameter default off — just say which branches. The code part of the change (the docs entry is in the commit): diff --git a/cfg.y b/cfg.y
@@ -385,2 +385,3 @@ extern int cfg_parse_only_routes;
%token TCP_KEEPALIVE
+%token REACTOR_DBG_FD_CHECK
%token TCP_KEEPCOUNT
@@ -1383,2 +1384,6 @@ assign_stm: LOGLEVEL EQUAL snumber { IFOR();
| TCP_KEEPALIVE EQUAL error { yyerror("boolean value expected"); }
+ | REACTOR_DBG_FD_CHECK EQUAL NUMBER { IFOR();
+ reactor_dbg_fd_check=!!$3;
+ }
+ | REACTOR_DBG_FD_CHECK EQUAL error { yyerror("boolean value expected"); }
| TCP_MAX_MSG_TIME EQUAL NUMBER { IFOR();
diff --git a/globals.c b/globals.c
@@ -81,2 +81,8 @@ int enable_asserts = 0;
#endif
+
+#ifdef EXTRA_DEBUG
+int reactor_dbg_fd_check = 1;
+#else
+int reactor_dbg_fd_check = 0;
+#endif
/* abort process on failed assertion. disabled by default */
diff --git a/io_wait.h b/io_wait.h
@@ -300,2 +300,4 @@ again:
+extern int reactor_dbg_fd_check;
+
#define check_io_data() \
@@ -305,2 +307,3 @@ again:
check_error = 0;\
+ if (!reactor_dbg_fd_check) break;\
/* iterate the fd_array and check if fd_hash is properly set for each */ \ |
|
Thanks @Lt-Flash |
io_wait: run the fd-map consistency check only with EXTRA_DEBUG (cherry picked from commit 92363ab)
io_wait: run the fd-map consistency check only with EXTRA_DEBUG (cherry picked from commit 92363ab)
Summary
check_io_data()inio_wait.his a consistency pass over the reactor's fd map — every one of themax_fd_noentries — and it runs on everyio_watch_add()andio_watch_del(). The macro is defined unconditionally, so the plain build pays it too.That makes each async registration or removal O(reactor size), i.e. O(
open_files_limit). Measured on a 16-core host with a 21k-entry reactor (open_files_limit65536, reactor shrunk to 20971 by the pkg budget): ~0.6 ms per call. A module doing a few thousand async operations a second — here cross-node cache pulls through the async framework, but any async DB or REST traffic takes the same path — spent about two cores in it:io_watch_add/io_watch_delwere the largest user-space symbols of aperfprofile at 13.6% of all CPU, and the receive buffers of the SIP sockets overflowed (129UdpRcvbufErrorsover a run) because the workers were busy walking the map.Change
The check is compiled only with
EXTRA_DEBUG; the plain build keepscheck_errorcleared and does nothing. No behaviour change otherwise.Effect
Same workload, same host, before → after: async-heavy worker CPU down by the two cores, UDP receive drops 129 → 0, request p95 in the early part of the run 0.97 → 0.91 ms; the check still runs in
EXTRA_DEBUGbuilds.Built with gcc and clang under
-Werror.