Skip to content

refactor(util): replace lock-free SPSCQueue with mutex-guarded ring buffer#145

Merged
K1ngst0m merged 1 commit intomainfrom
dev/refine-spsc
Mar 25, 2026
Merged

refactor(util): replace lock-free SPSCQueue with mutex-guarded ring buffer#145
K1ngst0m merged 1 commit intomainfrom
dev/refine-spsc

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Mar 25, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Queue validation now accepts a broader range of capacity values, improving flexibility in initialization.
  • Refactor

    • Internal queue implementation has been optimized for improved reliability and simplified maintenance while preserving the existing public API.

@qodo-code-review
Copy link
Copy Markdown

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Replace lock-free SPSCQueue with mutex-guarded ring buffer

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replace lock-free atomic operations with mutex-based synchronization
• Simplify capacity requirements from power-of-2 to any positive integer
• Use std::vector<std::optional<T>> for automatic memory management
• Update tests to reflect new capacity flexibility and remove power-of-2 constraints
Diagram
flowchart LR
  A["Lock-free atomics<br/>Power-of-2 capacity"] -->|Refactor| B["Mutex-based sync<br/>Any capacity"]
  C["Manual memory<br/>management"] -->|Simplify| D["std::vector<br/>std::optional"]
  E["Bitwise AND<br/>modulo"] -->|Replace| F["Modulo operator<br/>%"]
Loading

Grey Divider

File Changes

1. src/util/queues.hpp Refactoring +33/-62

Convert lock-free queue to mutex-based implementation

• Removed atomic operations (std::atomic<size_t>) and replaced with mutex-protected regular
 variables
• Changed capacity validation from power-of-2 requirement to simple > 0 check
• Replaced manual aligned memory allocation with std::vector<std::optional<T>>
• Simplified index calculations from bitwise AND to modulo operator
• Updated documentation to reflect new capacity requirements

src/util/queues.hpp


2. tests/util/test_queues.cpp 🧪 Tests +11/-8

Update tests for flexible capacity requirements

• Updated test section from "power-of-2 capacity" to "valid capacity"
• Changed non-power-of-2 tests from expecting exceptions to accepting valid capacities
• Added explicit test for zero capacity rejection
• Reorganized edge case tests by moving zero capacity check to construction tests

tests/util/test_queues.cpp


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 25, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Tests missing required includes 🐞 Bug ✓ Correctness
Description
tests/util/test_queues.cpp uses std::unique_ptr/std::make_unique and std::string but does not
include <memory> or <string>; this likely compiled only because queues.hpp previously included
<memory>. After this PR removes those transitive includes, the test can fail to compile depending on
the standard library/Catch2 headers.
Code

src/util/queues.hpp[R3-7]

#include <cstddef>
-#include <cstdlib>
-#include <memory>
+#include <mutex>
#include <optional>
#include <stdexcept>
-#include <type_traits>
+#include <vector>
Evidence
queues.hpp no longer includes <memory>, so test_queues.cpp can no longer rely on it transitively.
The test file uses std::unique_ptr/make_unique and std::string without including the corresponding
standard headers, which is not guaranteed to compile.

src/util/queues.hpp[1-8]
tests/util/test_queues.cpp[1-6]
tests/util/test_queues.cpp[63-94]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`tests/util/test_queues.cpp` uses `std::unique_ptr`, `std::make_unique`, and `std::string` but does not include `<memory>` / `<string>`. This likely became visible after `queues.hpp` stopped including `<memory>`.

### Issue Context
This is a compile portability issue caused by relying on transitive includes.

### Fix Focus Areas
- tests/util/test_queues.cpp[1-10]
 - Add `#include <memory>` and `#include <string>` (and any other directly-used standard headers not already included).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Missing <utility> include 🐞 Bug ✓ Correctness
Description
src/util/queues.hpp uses std::move but does not include <utility>, making the header
non-self-contained and potentially failing to compile on some toolchains/stdlib implementations.
Code

src/util/queues.hpp[R42-49]

    auto try_push(T&& item) -> bool {
-        const size_t current_head = m_head.load(std::memory_order_relaxed);
-        const size_t current_tail = m_tail.load(std::memory_order_acquire);
-        const size_t current_size = (current_head - current_tail) & m_capacity_mask;
-        if (current_size >= m_capacity) {
+        std::lock_guard lock(m_mutex);
+        if (m_size >= m_capacity) {
            return false;
        }
-
-        new (&m_buffer[current_head]) T(std::move(item));
-        const size_t next_head = (current_head + 1) & m_capacity_mask;
-        m_head.store(next_head, std::memory_order_release);
+        m_buffer[m_head].emplace(std::move(item));
+        m_head = (m_head + 1) % m_capacity;
+        ++m_size;
Evidence
The updated implementation calls std::move in try_push(T&&) and try_pop(), but the header's
include list does not include <utility>, which is where std::move is defined.

src/util/queues.hpp[1-8]
src/util/queues.hpp[42-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`src/util/queues.hpp` uses `std::move` but does not include `<utility>`, so the header is not self-contained.

### Issue Context
Relying on indirect includes (e.g., through `<optional>` or `<vector>`) is not guaranteed by the standard and can break builds across platforms/stdlibs.

### Fix Focus Areas
- src/util/queues.hpp[1-10]
 - Add `#include <utility>`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. try_* operations can block 🐞 Bug ➹ Performance
Description
SPSCQueue::try_push/try_pop now take a mutex with std::lock_guard, so they can block while another
thread holds the lock. This violates typical try_* non-blocking expectations and can add latency
in the compositor producer/consumer paths using these queues.
Code

src/util/queues.hpp[R31-61]

    auto try_push(const T& item) -> bool {
-        const size_t current_head = m_head.load(std::memory_order_relaxed);
-        const size_t current_tail = m_tail.load(std::memory_order_acquire);
-        const size_t current_size = (current_head - current_tail) & m_capacity_mask;
-        if (current_size >= m_capacity) {
+        std::lock_guard lock(m_mutex);
+        if (m_size >= m_capacity) {
            return false;
        }
-
-        new (&m_buffer[current_head]) T(item);
-        const size_t next_head = (current_head + 1) & m_capacity_mask;
-        m_head.store(next_head, std::memory_order_release);
+        m_buffer[m_head].emplace(item);
+        m_head = (m_head + 1) % m_capacity;
+        ++m_size;
        return true;
    }

    auto try_push(T&& item) -> bool {
-        const size_t current_head = m_head.load(std::memory_order_relaxed);
-        const size_t current_tail = m_tail.load(std::memory_order_acquire);
-        const size_t current_size = (current_head - current_tail) & m_capacity_mask;
-        if (current_size >= m_capacity) {
+        std::lock_guard lock(m_mutex);
+        if (m_size >= m_capacity) {
            return false;
        }
-
-        new (&m_buffer[current_head]) T(std::move(item));
-        const size_t next_head = (current_head + 1) & m_capacity_mask;
-        m_head.store(next_head, std::memory_order_release);
+        m_buffer[m_head].emplace(std::move(item));
+        m_head = (m_head + 1) % m_capacity;
+        ++m_size;
        return true;
    }

    auto try_pop() -> std::optional<T> {
-        const size_t current_tail = m_tail.load(std::memory_order_relaxed);
-        if (current_tail == m_head.load(std::memory_order_acquire)) {
+        std::lock_guard lock(m_mutex);
+        if (m_size == 0) {
            return std::nullopt;
        }
-
-        T item = std::move(m_buffer[current_tail]);
-        m_buffer[current_tail].~T();
-        const size_t next_tail = (current_tail + 1) & m_capacity_mask;
-        m_tail.store(next_tail, std::memory_order_release);
+        std::optional<T> item = std::move(m_buffer[m_tail]);
+        m_buffer[m_tail].reset();
+        m_tail = (m_tail + 1) % m_capacity;
+        --m_size;
Evidence
All queue operations take the same mutex, which can block under contention. The queue is used as a
cross-thread handoff between CompositorServer::inject_event (producer) and
CompositorState::process_input_events (consumer), so contention is plausible during event
bursts/draining loops.

src/util/queues.hpp[31-63]
src/compositor/compositor_input.cpp[296-363]
src/compositor/compositor_state.hpp[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`try_push` / `try_pop` can block due to unconditional mutex locking, which is surprising for `try_*` APIs and can introduce latency.

### Issue Context
These queues are used for compositor input/resize handoff. Blocking may be acceptable, but the API naming and documentation should match the behavior.

### Fix Focus Areas
- src/util/queues.hpp[31-63]
 - Either (a) rename APIs to non-`try_*` names and update call sites, or (b) explicitly document that `try_*` may block, or (c) redesign to preserve non-blocking `try_*` semantics without misclassifying contention as "full/empty".
- src/compositor/compositor_input.cpp[296-363]
 - Update usage/comments if API semantics change.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Threading guidance now inconsistent 🐞 Bug ⚙ Maintainability
Description
docs/threading.md recommends util::SPSCQueue for cross-thread handoff and also warns to keep
blocking synchronization out of the per-frame path; after this PR, SPSCQueue is a blocking (mutex)
primitive. The guide now gives contradictory guidance about when SPSCQueue is appropriate.
Code

src/util/queues.hpp[83]

+    mutable std::mutex m_mutex;
Evidence
The queue now uses a mutex, so it constitutes blocking synchronization under contention. The
threading guide explicitly calls out avoiding blocking sync in per-frame paths while simultaneously
promoting SPSCQueue for cross-thread communication use cases (including compositor queues).

docs/threading.md[67-74]
src/util/queues.hpp[31-84]
src/compositor/compositor_state.hpp[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The threading guide's guidance about SPSCQueue and avoiding blocking synchronization is now inconsistent with the mutex-based implementation.

### Issue Context
The docs currently steer developers toward SPSCQueue in places where blocking may be discouraged.

### Fix Focus Areas
- docs/threading.md[67-74]
 - Update the section to reflect that `util::SPSCQueue` is mutex-based and may block, and clarify when it is acceptable vs. when a non-blocking structure is required.
- docs/architecture.md[60-73]
 - Keep terminology consistent with threading.md.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

The SPSCQueue implementation was refactored from a lock-free atomic-based ring buffer to a mutex-guarded queue using std::vector<std::optional<T>>. Capacity validation was relaxed from requiring power-of-two to requiring only positive values. Index management transitioned from atomic operations to non-atomic tracking protected by a mutex, with explicit size counters replacing derived computations.

Changes

Cohort / File(s) Summary
Queue Implementation
src/util/queues.hpp
Replaced lock-free ring buffer with mutex-guarded fixed-capacity queue; changed storage from manually allocated buffer to std::vector<std::optional<T>>; relaxed capacity validation from power-of-two requirement to capacity > 0; replaced atomic index operations with non-atomic indices protected by std::lock_guard; simplified destructor to default implementation.
Queue Tests
tests/util/test_queues.cpp
Updated capacity validation tests to accept non-power-of-two values; added explicit zero-capacity validation test; renamed "power-of-2 capacity" section to "valid capacity"; removed zero-capacity edge-case from boundary tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A queue once danced with atoms bright,
Now sheltered safe by mutex's might,
No power-of-two constraints to bind,
Just vector storage, peace of mind,
Simpler, clearer, lock it tight! 🔒

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring: replacing a lock-free SPSC queue implementation with a mutex-guarded version.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/refine-spsc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/util/queues.hpp (3)

17-22: Capacity validation occurs after partial initialization.

The check if (capacity == 0) runs after m_buffer(capacity) is already initialized in the member initializer list. While this works correctly (a zero-size vector is valid and will be cleaned up when the exception propagates), validating before initialization is cleaner.

♻️ Optional: Use a helper to validate early
+private:
+    static size_t validate_capacity(size_t cap) {
+        if (cap == 0) {
+            throw std::invalid_argument("SPSCQueue capacity must be > 0");
+        }
+        return cap;
+    }
+
+public:
     explicit SPSCQueue(size_t capacity)
-        : m_capacity(capacity), m_buffer(capacity), m_head(0), m_tail(0), m_size(0) {
-        if (capacity == 0) {
-            throw std::invalid_argument("SPSCQueue capacity must be > 0");
-        }
+        : m_capacity(validate_capacity(capacity)), m_buffer(m_capacity), m_head(0), m_tail(0), m_size(0) {
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/queues.hpp` around lines 17 - 22, Validate the capacity before any
member initializers run in the SPSCQueue constructor to avoid partially
initializing members (m_buffer, m_capacity, m_head, m_tail, m_size) when
capacity is invalid; move the check for capacity == 0 out of the member
initializer list so it executes first (or introduce a small static/constexpr
validate_capacity(size_t) helper called in the initializer) and throw
std::invalid_argument if zero, then initialize m_buffer(capacity) and the other
members normally in the constructor initializer list.

11-15: Class name and documentation no longer reflect implementation.

The class is named SPSCQueue (Single-Producer, Single-Consumer) and documented as such, but the mutex-based implementation is now thread-safe for arbitrary numbers of producers and consumers. This is misleading since:

  1. Users may expect lock-free SPSC performance characteristics
  2. The name doesn't convey the actual thread-safety guarantees

Consider renaming to BoundedQueue or ThreadSafeQueue, or updating the documentation to clarify the mutex-based implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/queues.hpp` around lines 11 - 15, The class name SPSCQueue and its
documentation are misleading because the implementation uses a mutex and
supports multiple producers/consumers; rename the class to a name that reflects
its behavior (e.g., BoundedQueue or ThreadSafeQueue) and update the doc comment
to state it is a bounded, mutex-protected queue supporting multiple producers
and consumers and that capacity must be > 0 (construction throws
std::invalid_argument). Update all references/usages of SPSCQueue in the
codebase (constructors, type aliases, tests, and template instantiations) to the
new class name and adjust any comments mentioning “single-producer
single-consumer” to describe the actual thread-safety and performance
characteristics.

65-73: Consider whether size() and empty() need locking.

Both methods acquire the mutex for reading m_size. This is correct for consistency, but note that the returned value may be stale immediately after the lock is released. If callers use these for decision-making (e.g., "if not empty, pop"), they should use try_pop() directly instead.

If these methods are only used for diagnostics/debugging and don't need strong consistency, making m_size atomic and removing the lock here could reduce contention.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/queues.hpp` around lines 65 - 73, size() and empty() currently lock
m_mutex to read m_size which is safe but can cause contention and still produce
stale results after the lock is released; decide whether you need strong
consistency for callers and either (A) keep the mutex usage and add
documentation on using try_pop() for decision-making (e.g., "if not empty, pop"
race), or (B) make m_size an std::atomic<size_t> and remove the locking in
size() and empty() to reduce contention for diagnostic-only uses; update the
implementation of size(), empty(), and any comments accordingly and reference
the members/functions m_size, m_mutex, size(), empty(), and try_pop() when
applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/util/queues.hpp`:
- Around line 17-22: Validate the capacity before any member initializers run in
the SPSCQueue constructor to avoid partially initializing members (m_buffer,
m_capacity, m_head, m_tail, m_size) when capacity is invalid; move the check for
capacity == 0 out of the member initializer list so it executes first (or
introduce a small static/constexpr validate_capacity(size_t) helper called in
the initializer) and throw std::invalid_argument if zero, then initialize
m_buffer(capacity) and the other members normally in the constructor initializer
list.
- Around line 11-15: The class name SPSCQueue and its documentation are
misleading because the implementation uses a mutex and supports multiple
producers/consumers; rename the class to a name that reflects its behavior
(e.g., BoundedQueue or ThreadSafeQueue) and update the doc comment to state it
is a bounded, mutex-protected queue supporting multiple producers and consumers
and that capacity must be > 0 (construction throws std::invalid_argument).
Update all references/usages of SPSCQueue in the codebase (constructors, type
aliases, tests, and template instantiations) to the new class name and adjust
any comments mentioning “single-producer single-consumer” to describe the actual
thread-safety and performance characteristics.
- Around line 65-73: size() and empty() currently lock m_mutex to read m_size
which is safe but can cause contention and still produce stale results after the
lock is released; decide whether you need strong consistency for callers and
either (A) keep the mutex usage and add documentation on using try_pop() for
decision-making (e.g., "if not empty, pop" race), or (B) make m_size an
std::atomic<size_t> and remove the locking in size() and empty() to reduce
contention for diagnostic-only uses; update the implementation of size(),
empty(), and any comments accordingly and reference the members/functions
m_size, m_mutex, size(), empty(), and try_pop() when applying the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 836afc08-12b5-4c10-a9fb-af78dfa5957f

📥 Commits

Reviewing files that changed from the base of the PR and between 2bdb712 and 57da0ed.

📒 Files selected for processing (2)
  • src/util/queues.hpp
  • tests/util/test_queues.cpp

Comment on lines 3 to +7
#include <cstddef>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <vector>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Tests missing required includes 🐞 Bug ✓ Correctness

tests/util/test_queues.cpp uses std::unique_ptr/std::make_unique and std::string but does not
include <memory> or <string>; this likely compiled only because queues.hpp previously included
<memory>. After this PR removes those transitive includes, the test can fail to compile depending on
the standard library/Catch2 headers.
Agent Prompt
### Issue description
`tests/util/test_queues.cpp` uses `std::unique_ptr`, `std::make_unique`, and `std::string` but does not include `<memory>` / `<string>`. This likely became visible after `queues.hpp` stopped including `<memory>`.

### Issue Context
This is a compile portability issue caused by relying on transitive includes.

### Fix Focus Areas
- tests/util/test_queues.cpp[1-10]
  - Add `#include <memory>` and `#include <string>` (and any other directly-used standard headers not already included).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 42 to +49
auto try_push(T&& item) -> bool {
const size_t current_head = m_head.load(std::memory_order_relaxed);
const size_t current_tail = m_tail.load(std::memory_order_acquire);
const size_t current_size = (current_head - current_tail) & m_capacity_mask;
if (current_size >= m_capacity) {
std::lock_guard lock(m_mutex);
if (m_size >= m_capacity) {
return false;
}

new (&m_buffer[current_head]) T(std::move(item));
const size_t next_head = (current_head + 1) & m_capacity_mask;
m_head.store(next_head, std::memory_order_release);
m_buffer[m_head].emplace(std::move(item));
m_head = (m_head + 1) % m_capacity;
++m_size;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Missing include 🐞 Bug ✓ Correctness

src/util/queues.hpp uses std::move but does not include <utility>, making the header
non-self-contained and potentially failing to compile on some toolchains/stdlib implementations.
Agent Prompt
### Issue description
`src/util/queues.hpp` uses `std::move` but does not include `<utility>`, so the header is not self-contained.

### Issue Context
Relying on indirect includes (e.g., through `<optional>` or `<vector>`) is not guaranteed by the standard and can break builds across platforms/stdlibs.

### Fix Focus Areas
- src/util/queues.hpp[1-10]
  - Add `#include <utility>`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@K1ngst0m K1ngst0m merged commit 9532e82 into main Mar 25, 2026
5 checks passed
@K1ngst0m K1ngst0m deleted the dev/refine-spsc branch March 25, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant