From 349d80c64361df8d0aea8c695cf1549445088ddb Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Thu, 29 Oct 2026 11:46:19 -0500 Subject: [PATCH] perf(index): run supervised index workers at below-normal CPU priority A full index saturates every core in the parallel extraction pass, and interactive CLI/MCP queries (CPU-bound at start-up) were measured taking twice their wall time behind it with flat CPU time: pure run-queue contention. The index-worker role now lowers its own priority once, before any indexing thread is created: nice 10 on POSIX, BELOW_NORMAL_PRIORITY_CLASS on Windows. It never raises priority: a process already nicer than 10 (or in the IDLE / BELOW_NORMAL class) is left alone. Plain nice only, not macOS background QoS, which would pin indexing to efficiency cores and throttle its I/O. Failure is logged and ignored. The test runs the helper in a forked child and checks both legs: a normal-priority child ends at nice >= 10, and a child started nicer than that is left where it was. Refs #2834 Signed-off-by: Shane McCarron --- src/foundation/platform.c | 25 +++++++++++++++++++++ src/foundation/platform.h | 13 +++++++++++ src/main.c | 6 +++++ tests/test_platform.c | 47 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/src/foundation/platform.c b/src/foundation/platform.c index d11df686e..3a4ac1642 100644 --- a/src/foundation/platform.c +++ b/src/foundation/platform.c @@ -729,3 +729,28 @@ const char *cbm_resolve_cache_dir(void) { } return buf; } + +/* ── CPU scheduling priority ────────────────── */ + +#ifndef _WIN32 +#include +#endif + +bool cbm_lower_cpu_priority(void) { +#ifdef _WIN32 + HANDLE self = GetCurrentProcess(); + DWORD current = GetPriorityClass(self); + if (current == IDLE_PRIORITY_CLASS || current == BELOW_NORMAL_PRIORITY_CLASS) { + return true; /* already at least this low: never raise priority */ + } + return SetPriorityClass(self, BELOW_NORMAL_PRIORITY_CLASS) != 0; +#else + /* getpriority can legitimately return -1, so errno is the only error signal. */ + errno = 0; + int current = getpriority(PRIO_PROCESS, 0); + if (errno == 0 && current >= CBM_LOW_CPU_PRIORITY_NICE) { + return true; /* already at least this nice: never raise priority */ + } + return setpriority(PRIO_PROCESS, 0, CBM_LOW_CPU_PRIORITY_NICE) == 0; +#endif +} diff --git a/src/foundation/platform.h b/src/foundation/platform.h index 40b020222..8eb462778 100644 --- a/src/foundation/platform.h +++ b/src/foundation/platform.h @@ -215,4 +215,17 @@ int64_t cbm_file_size(const char *path); * On POSIX, this is a no-op. Returns the input pointer. */ char *cbm_normalize_path_sep(char *path); +/* ── CPU scheduling priority ───────────────────────────────────── */ + +enum { CBM_LOW_CPU_PRIORITY_NICE = 10 }; + +/* Lower this process's CPU scheduling priority so background work yields the + * cores to interactive processes. POSIX: nice CBM_LOW_CPU_PRIORITY_NICE (never + * raises an already-nicer process). Plain nice only, deliberately not macOS + * background QoS: that pins work to efficiency cores and throttles I/O. + * Windows: BELOW_NORMAL_PRIORITY_CLASS. On Linux nice is per thread, so call + * this before creating threads; later threads inherit it. + * Returns false when the priority could not be changed. */ +bool cbm_lower_cpu_priority(void); + #endif /* CBM_PLATFORM_H */ diff --git a/src/main.c b/src/main.c index c2f22ada9..3f5e80e22 100644 --- a/src/main.c +++ b/src/main.c @@ -902,6 +902,12 @@ static int run_cli(int argc, char **argv, cbm_project_lock_manager_t *project_lo * own here, and only here (cbm_sqlite_dedicated_heap). */ cbm_sqlite_dedicated_heap(true); cbm_memev_process_role("worker"); + /* A full index saturates every core; run it below normal priority so + * interactive CLI/MCP queries are not starved. Before any indexing + * thread exists, so they all inherit it. Failure is harmless. */ + if (!cbm_lower_cpu_priority()) { + cbm_log_info("index.worker.priority", "lowered", "false"); + } } cbm_index_set_worker_role_options(index_worker, response_out, worker_single_thread, worker_marker, worker_quarantine, diff --git a/tests/test_platform.c b/tests/test_platform.c index 2c383c025..d83f90dab 100644 --- a/tests/test_platform.c +++ b/tests/test_platform.c @@ -16,6 +16,9 @@ #include #ifndef _WIN32 #include /* chmod for the symlink-policy mode legs */ +#include +#include /* getpriority for the worker-priority leg */ +#include #endif #ifdef __linux__ @@ -1442,6 +1445,49 @@ TEST(regex_estimate_counts_brackets_and_escapes_literally) { PASS(); } +/* An index worker lowers its own priority (#2834). A non-root process cannot + * raise it back, so the helper runs in a forked child that reports its nice. */ +TEST(platform_lower_cpu_priority_nices_the_process) { +#ifdef _WIN32 + SKIP_PLATFORM("POSIX nice contract"); +#else + int fds[2]; + ASSERT_EQ(pipe(fds), 0); + pid_t child = fork(); + if (child == 0) { + (void)close(fds[0]); + /* report: lowered ok, nice after; then, from a nicer start, ok, target, nice after */ + int report[5] = {cbm_lower_cpu_priority() ? 1 : 0, 0, 0, 0, 0}; + errno = 0; + report[1] = getpriority(PRIO_PROCESS, 0); + if (errno != 0) { + report[0] = 0; + } + /* Never-raise leg: start nicer than the helper's level; it must leave it. */ + report[3] = report[1] >= 15 ? 19 : 15; + if (setpriority(PRIO_PROCESS, 0, report[3]) == 0) { + report[2] = cbm_lower_cpu_priority() ? 1 : 0; + errno = 0; + report[4] = getpriority(PRIO_PROCESS, 0); + } + _exit(write(fds[1], report, sizeof(report)) == (ssize_t)sizeof(report) ? 0 : 1); + } + ASSERT_TRUE(child > 0); + (void)close(fds[1]); + int report[5] = {0, 0, 0, 0, 0}; + ssize_t got = read(fds[0], report, sizeof(report)); + (void)close(fds[0]); + int status = 0; + (void)waitpid(child, &status, 0); + ASSERT_EQ(got, (ssize_t)sizeof(report)); + ASSERT_EQ(report[0], 1); + ASSERT_TRUE(report[1] >= CBM_LOW_CPU_PRIORITY_NICE); + ASSERT_EQ(report[2], 1); + ASSERT_EQ(report[4], report[3]); + PASS(); +#endif +} + SUITE(platform) { RUN_TEST(platform_file_apis_survive_max_path_overflow); RUN_TEST(platform_mkdir_p_follows_own_symlink_only_when_opted_in); @@ -1477,6 +1523,7 @@ SUITE(platform) { RUN_TEST(platform_env_long_refuses_what_it_cannot_read); RUN_TEST(platform_env_dup_honours_the_settings_resolver); RUN_TEST(platform_system_info); + RUN_TEST(platform_lower_cpu_priority_nices_the_process); RUN_TEST(regex_compile_refuses_oversized_expansion); RUN_TEST(regex_compile_accepts_expansion_under_budget); RUN_TEST(regex_compile_refuses_overlong_pattern);