From ea7ec561da9a0a2999ec2b7c26105cbf1426538e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 11 Oct 2026 05:57:16 -0700 Subject: [PATCH] Grow the descriptor table before worker threads share it Every worker mailbox is a pipe. Once a process passes 64 descriptors the kernel grows its descriptor table, and while other threads share the table expand_fdtable first waits a full RCU grace period. tsc-ts on a 16-vCPU host stalled 11-25 ms on the thread starting its 14th checker (16 parse threads plus 13 checkers), on the critical path. Worker builds now grow the table to 256 entries at load, while the process has one thread, which never waits; the table never shrinks. --- packages/runtime/src/scr_worker.c | 27 ++++++++++++++++++++++++++ packages/runtime/src/scr_worker.test.c | 25 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/packages/runtime/src/scr_worker.c b/packages/runtime/src/scr_worker.c index 18312cef5..d9927e8c6 100644 --- a/packages/runtime/src/scr_worker.c +++ b/packages/runtime/src/scr_worker.c @@ -11,6 +11,33 @@ #include #endif +#if defined(__linux__) +#include +#include +#include + +/* Every worker's mailbox holds descriptors. Once a process passes 64 of + * them, the kernel grows its descriptor table, and while other threads share + * the table it first waits a full RCU grace period (synchronize_rcu in + * expand_fdtable): measured 11-25 ms on a 16-vCPU host, on the thread that + * starts the next worker. Growing the table at load, while the process still + * has one thread, never waits; the table never shrinks. 256 entries leave + * room for the most workers the allocator gives arenas to. */ +__attribute__((constructor)) static void scr_worker_reserve_fd_table(void) { + const int top = 255; + struct rlimit limit; + if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur <= (rlim_t)top) + return; + for (int fd = 0; fd <= 2; fd++) { + int copy = fcntl(fd, F_DUPFD_CLOEXEC, top); + if (copy >= 0) { + (void)close(copy); + return; + } + } +} +#endif + struct ScrWorkerThread { atomic_size_t references; atomic_bool cancel; diff --git a/packages/runtime/src/scr_worker.test.c b/packages/runtime/src/scr_worker.test.c index f8ecee48a..12065b43d 100644 --- a/packages/runtime/src/scr_worker.test.c +++ b/packages/runtime/src/scr_worker.test.c @@ -54,7 +54,32 @@ static int entry(uint32_t root_id) { return (int)root_id; } +#if defined(__linux__) +#include +#include +#include + +/* Worker builds grow the descriptor table at load, while the process has one + * thread: growing it later, with threads sharing it, waits a full RCU grace + * period on the thread that opens the next worker's mailbox. */ +static void check_fd_table_reserved(void) { + struct rlimit limit; + if (getrlimit(RLIMIT_NOFILE, &limit) != 0 || (limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur <= 255)) return; + FILE *status = fopen("/proc/self/status", "r"); + assert(status); + char line[256]; + long size = -1; + while (fgets(line, sizeof line, status)) + if (strncmp(line, "FDSize:", 7) == 0) size = strtol(line + 7, NULL, 10); + fclose(status); + assert(size >= 256); +} +#endif + int main(void) { +#if defined(__linux__) + check_fd_table_reserved(); +#endif scr_init(); scr_lib_init(0, NULL); ScrMailbox *mailbox = scr_mailbox_new();