Skip to content

Commit da3872f

Browse files
Fix Reader Thread Scaling Bottlenecks due to PausePoint Mutex (valkey-io#1037)
## Fix Reader Thread Scaling Bottleneck: PausePoint Mutex Contention ## Problem Adding reader threads **degrades** performance instead of improving it: | Reader Threads | TEXT 10K (req/s) | NUMERIC 10K (req/s) | TAG 10K (req/s) | |:-:|:-:|:-:|:-:| | 1 | 1,195 | 2,229 | 1,588 | | 2 | 538 (↓55%) | 573 (↓74%) | 578 (↓64%) | | 4 | 415 (↓65%) | 409 (↓82%) | 377 (↓76%) | | 8 | 440 (↓63%) | 510 (↓77%) | 465 (↓71%) | | 16 | 459 (↓62%) | 523 (↓77%) | 433 (↓73%) | Performance drops 2-3x at 2 threads and plateaus at 4+. Reader threads are actively harmful. ## Root Cause (perf proof) ``` Baseline perf profile (8 reader threads, TEXT 10K): 61.28% absl::Mutex::Lock 11.17% absl::Mutex::Unlock 4.09% SearchNonVectorQuery 2.54% InternedString::DecrementRefCount ``` `BACKGROUND_PAUSEPOINT` is called multiple times per query on every reader thread. It unconditionally calls `PausePoint()` which locks a single global `absl::Mutex`. All threads serialize on this lock — 72% of CPU is spent waiting for it. ## Fix Skip the mutex entirely when no pause points are registered (the normal production case). A plain `bool` flag is set to `true` only when `FT._DEBUG` registers a pause point. The macros check this flag inline before calling into the mutex-protected path. ```cpp // BEFORE: always calls PausePoint() → always locks mutex #define BACKGROUND_PAUSEPOINT(name) \ if (!vmsdk::IsMainThread()) { \ PAUSEPOINT(name); \ } // AFTER: skip entirely when no pause points are active #define BACKGROUND_PAUSEPOINT(name) \ do { \ if (vmsdk::debug::pause_points_enabled && !vmsdk::IsMainThread()) { \ PAUSEPOINT(name); \ } \ } while (false) ``` The flag is non-atomic — it's only written by the main thread during `FT._DEBUG` commands and read by reader threads. This is safe because: - False negatives (reading stale `false` after a pause point is set) only delay the pause point taking effect by one query — acceptable for a debug tool. - False positives cannot occur — once set to `true`, it stays `true`. ## Results Benchmark: 500 clients, 30 seconds per test, core-pinned (server cores 0-31, client cores 32-63). ### TEXT — 10K matches (req/s) | Reader Threads | Baseline | + PausePoint Fix | vs Baseline | |:-:|:-:|:-:|:-:| | 1 | 1,195 | 1,581 | 1.3x | | 2 | 538 | 3,012 | 5.6x | | 4 | 415 | 5,633 | 13.6x | | 8 | 440 | 7,564 | 17.2x | | 16 | 459 | 9,068 | 19.8x | ### NUMERIC — 10K matches (req/s) | Reader Threads | Baseline | + PausePoint Fix | vs Baseline | |:-:|:-:|:-:|:-:| | 1 | 2,229 | 4,094 | 1.8x | | 2 | 573 | 7,047 | 12.3x | | 4 | 409 | 13,702 | 33.5x | | 8 | 510 | 18,208 | 35.7x | | 16 | 523 | 10,447 | 20.0x | ### TAG — 10K matches (req/s) | Reader Threads | Baseline | + PausePoint Fix | vs Baseline | |:-:|:-:|:-:|:-:| | 1 | 1,588 | 2,336 | 1.5x | | 2 | 578 | 3,245 | 5.6x | | 4 | 377 | 5,092 | 13.5x | | 8 | 465 | 9,354 | 20.1x | | 16 | 433 | 10,912 | 25.2x | ### 100 matches — no regression | Reader Threads | TEXT Baseline | TEXT Fixed | NUMERIC Baseline | NUMERIC Fixed | TAG Baseline | TAG Fixed | |:-:|:-:|:-:|:-:|:-:|:-:|:-:| | 1 | 86,671 | 85,061 | 100,574 | 95,488 | 91,206 | 95,206 | | 2 | 59,449 | 84,228 | 63,224 | 92,685 | 61,895 | 94,552 | | 4 | 44,872 | 80,494 | 40,106 | 79,978 | 40,269 | 89,594 | | 8 | 44,661 | 72,067 | 46,526 | 78,173 | 45,405 | 78,578 | | 16 | 45,542 | 72,358 | 46,547 | 74,804 | 42,161 | 76,501 | Light queries also improve significantly at 2+ threads (no longer serializing on the mutex). ## After Fix — Perf Profile (16T, TEXT 10K) ``` 44.05% InternedString::DecrementRefCount 14.80% TermIterator::InsertValidKeyIterator 12.54% TermIterator::FindMinimumValidKey 10.78% SearchNonVectorQuery 0.00% absl::Mutex::Lock ``` Zero mutex contention. The next bottleneck is `DecrementRefCount` at 44% — atomic ref counting on `InternedStringPtr` during iteration causes cache-line bouncing across cores. I am addressing this in a follow-up. ## Impact - Reader thread scaling works as intended — performance improves with additional threads instead of degrading. - Heavy queries (10K matches) achieve up to 20x improvement at 16 threads vs baseline. - No regression at 1 thread for any query type. - Light queries (100 matches) also improve at 2+ threads. --------- Signed-off-by: Karthik Subbarao <karthikrs2021@gmail.com>
1 parent 7cb0d02 commit da3872f

2 files changed

Lines changed: 23 additions & 6 deletions

File tree

vmsdk/src/debug.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ struct Waiter {
2929
};
3030

3131
absl::flat_hash_map<std::string, std::vector<Waiter>> pause_point_waiters;
32+
// Global flag: set to true when any pause point is registered.
33+
bool pause_points_enabled{false};
3234

3335
static std::string ToString(const std::source_location& location) {
3436
std::string os;
@@ -85,6 +87,7 @@ void PausePointControl(absl::string_view point, bool enable) {
8587
if (enable) {
8688
if (!pause_point_waiters.contains(point)) {
8789
pause_point_waiters[point];
90+
pause_points_enabled = true;
8891
}
8992
CHECK(pause_point_waiters.contains(point));
9093
} else {
@@ -139,6 +142,9 @@ void PausePointList(ValkeyModuleCtx* ctx) {
139142
void ClearAllPausePoints() {
140143
absl::MutexLock lock(&pause_point_lock);
141144
pause_point_waiters.clear();
145+
// Note: pause_points_enabled stays true. No reason to reset it —
146+
// it's only checked as a fast-path skip in production where no
147+
// pause points are ever set.
142148
}
143149

144150
//

vmsdk/src/debug.h

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
namespace vmsdk {
2222
namespace debug {
2323

24+
// Global flag: set to true when any pause point is registered via FT._DEBUG.
25+
// Non-atomic — only written by the main thread during FT._DEBUG commands.
26+
// Checked inline by PAUSEPOINT/BACKGROUND_PAUSEPOINT macros for fast skip.
27+
extern bool pause_points_enabled;
28+
2429
//
2530
// PausePoints are a tool to help with debugging of background processes.
2631
//
@@ -30,14 +35,20 @@ namespace debug {
3035
// current thread. A unique label is provided to distinguish this pause point
3136
// with others.
3237
//
33-
#define PAUSEPOINT(name) \
34-
vmsdk::debug::PausePoint(name, std::source_location::current())
38+
#define PAUSEPOINT(name) \
39+
do { \
40+
if (vmsdk::debug::pause_points_enabled) { \
41+
vmsdk::debug::PausePoint(name, std::source_location::current()); \
42+
} \
43+
} while (false)
3544
void PausePoint(absl::string_view point, std::source_location location);
3645

37-
#define BACKGROUND_PAUSEPOINT(name) \
38-
if (!vmsdk::IsMainThread()) { \
39-
PAUSEPOINT(name); \
40-
} \
46+
#define BACKGROUND_PAUSEPOINT(name) \
47+
do { \
48+
if (vmsdk::debug::pause_points_enabled && !vmsdk::IsMainThread()) { \
49+
PAUSEPOINT(name); \
50+
} \
51+
} while (false)
4152
//
4253
// This function is used by the control machinery (FT.DEBUG) to enable/disable
4354
// and test PausePoints.

0 commit comments

Comments
 (0)