From f2329e060585d0031cd5a81ff87a59c8706e179c Mon Sep 17 00:00:00 2001 From: Stefano Maxenti Date: Fri, 18 Sep 2026 15:42:49 -0400 Subject: [PATCH] c_api: let C callers pin the I/O threads E3Config has carried io_thread_affinity since the real-time data path landed, but e3_config_t never exposed it, so the C API -- and therefore the OAI integration, its only C consumer -- had no way to reach it. The inbound, outbound and report loops ran wherever the scheduler put them. Expose it as a pair: pin_io_threads gates the setting and io_thread_affinity carries the core. The other optional fields use an in-band sentinel (-1 or 0 meaning "keep the default"), which does not work here because every core index including 0 is a legitimate target; with a flag, a zero-initialized e3_config_t means "do not pin" rather than "pin to core 0". That distinction matters on a host with isolated cores, where core 0 is typically the housekeeping core the RAN most wants to stay off. While here, stop swallowing the outcome in apply_thread_config(). A requested core outside the process's cpuset is the expected failure on such a host, and discarding the return made a missed pin look exactly like a working one. Both the affinity and the niceness call stay best-effort, but now say what happened. Assisted-by: Claude:claude-opus-5 --- VERSION | 2 +- include/libe3/c_api.h | 3 +++ src/c_api.cpp | 3 +++ src/core/e3_interface.cpp | 19 +++++++++++++++---- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index 0ea3a944..0c62199f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 diff --git a/include/libe3/c_api.h b/include/libe3/c_api.h index 2c7b6fad..abb4f29d 100644 --- a/include/libe3/c_api.h +++ b/include/libe3/c_api.h @@ -179,6 +179,9 @@ typedef struct { size_t io_threads; /* 0=default */ int log_level; /* -1=default */ const char* log_path; /* NULL=auto (/tmp/e3_[_]_.log) */ + /* A flag rather than a sentinel: core 0 is a legitimate target. */ + int pin_io_threads; /* 0=no pinning */ + int io_thread_affinity; /* logical core; Linux only, best-effort */ } e3_config_t; /** diff --git a/src/c_api.cpp b/src/c_api.cpp index 1905923d..6834f565 100644 --- a/src/c_api.cpp +++ b/src/c_api.cpp @@ -207,6 +207,9 @@ e3_agent_handle_t* e3_agent_create_with_config(const e3_config_t* config) { if (config->io_threads != 0) cfg.io_threads = config->io_threads; if (config->log_level >= 0) cfg.log_level = config->log_level; if (config->log_path) cfg.log_path = config->log_path; + // Gated by the flag, not a sentinel: core 0 is a real target. + if (config->pin_io_threads) + cfg.io_thread_affinity = config->io_thread_affinity; } e3_agent_handle_t* h = new e3_agent_handle_s(); h->agent = std::make_unique(std::move(cfg)); diff --git a/src/core/e3_interface.cpp b/src/core/e3_interface.cpp index 04d19685..98bb17eb 100644 --- a/src/core/e3_interface.cpp +++ b/src/core/e3_interface.cpp @@ -12,9 +12,11 @@ #include "libe3/logger.hpp" #include "libe3/latrec.h" #include +#include #include #include #include +#include #include #include @@ -79,9 +81,15 @@ void apply_thread_config(int affinity, int niceness, const char* role) noexcept cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(static_cast(affinity), &cpuset); - // Non-fatal if this fails (e.g. core index out of range). - (void)pthread_setaffinity_np(pthread_self(), - sizeof(cpu_set_t), &cpuset); + // Non-fatal, but say so: a silent miss looks identical to a working pin. + int rc = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset); + if (rc != 0) { + E3_LOG_WARN(LOG_TAG) << "Failed to pin " << role << " thread to core " + << affinity << ": " << std::strerror(rc) + << " (thread left unpinned)"; + } else { + E3_LOG_INFO(LOG_TAG) << "Pinned " << role << " thread to core " << affinity; + } } if (niceness != 0) { // Use the kernel TID so only this thread's scheduling priority changes. @@ -89,7 +97,10 @@ void apply_thread_config(int affinity, int niceness, const char* role) noexcept // to pid_t first, then to the id_t expected by setpriority(PRIO_PROCESS). auto tid = static_cast(static_cast(syscall(SYS_gettid))); // Non-fatal: negative values require CAP_SYS_NICE. - (void)setpriority(PRIO_PROCESS, tid, niceness); + if (setpriority(PRIO_PROCESS, tid, niceness) == -1) { + E3_LOG_WARN(LOG_TAG) << "Failed to set niceness " << niceness << " on " + << role << " thread: " << std::strerror(errno); + } } #else (void)affinity;