Skip to content

feat: add MutexQueue v1 with tests and benchmarks - #2

Merged
jadecubes merged 10 commits into
mainfrom
feat/mutex-queue
Aug 17, 2026
Merged

feat: add MutexQueue v1 with tests and benchmarks#2
jadecubes merged 10 commits into
mainfrom
feat/mutex-queue

Conversation

@jadecubes

@jadecubes jadecubes commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Adds the v1 baseline queue: a bounded FIFO ring guarded by a single mutex, with its tests and benchmarks. Was stacked on #1 (now merged); main has since been merged back in and the ci.yml conflict resolved. Verified locally: all tests pass under TSan; clang-format and clang-tidy clean.

Changed files

File What changed
include/cq/mutex_queue.hpp New — MutexQueue<T> public API: fixed-capacity ring, one mutex + not_full/not_empty condition variables, blocking push/pop, non-blocking try_ variants, close() shutdown semantics; copy/move deleted; class doc states the thread-safety contract
include/cq/mutex_queue.ipp New — member definitions, included from the header (template bodies must be visible at instantiation). Wait predicates evaluate each condition once per check; close() is idempotent and skips re-notifying
tests/queue_test_util.hpp New — shared thread harness: kSettleTime, run_blocked (spawn → settle → unblock → join), spawn_threads; reused by both test files and by future queue variants' tests
tests/mutex_queue_test.cpp New — 13 unit tests: FIFO order, ring wraparound, try_ failure paths, move-only element types, blocking behaviour and close-wakes via run_blocked
tests/stress_test.cpp New — 4-producer / 4-consumer checksum stress test (100k items through a 64-slot ring), run under ThreadSanitizer in CI
tests/CMakeLists.txt New — queue_tests target; discovery mode comes from the root (PRE_TEST, so the TSan binary never runs at build time)
tests/.clang-tidy New — narrows readability-identifier-length to specific idiomatic names (q, p, c, …) instead of disabling the check
bench/queue_bench.cpp New — throughput benchmarks: SPSC (1+1) and MPMC (4+4) via one registration, plus a single-thread round trip. One queue op per harness iteration so --benchmark_min_time/1x smoke runs scale properly; odd thread counts rejected with SkipWithError
bench/CMakeLists.txt New — queue_bench target plus a one-iteration ctest smoke run
bench/.clang-tidy New — suppresses DeadStores on the canonical for (auto _ : state) loop; allows BM_ names
CMakeLists.txt Drops the EXISTS guards (both directories exist as of this PR); cq links Threads::Threads; project-wide PRE_TEST gtest discovery
.github/workflows/ci.yml Hardcodes --no-tests=error — the dynamic "does the directory exist" guard from the scaffolding era is obsolete. Conflict with main resolved via merge

🤖 Generated with Claude Code

Debra and others added 2 commits August 16, 2026 16:31
Bounded mutex+condvar FIFO ring (v1 baseline), its GoogleTest unit and
TSan stress tests, and Google Benchmark targets. The CMake scaffolding
from the tooling PR picks these directories up automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- factor the hand-negated full/empty conditions into can_enqueue_locked /
  can_dequeue_locked so each contract exists once
- drop the now-unneeded EXISTS scaffolding guards and make ctest fail on
  zero discovered tests (--no-tests=error): a missing directory should
  fail loudly now that the sources exist
- shared benchmark queue: optional + defensive guard -> unique_ptr
  (bugprone-unchecked-optional-access wants the guard the review flagged
  as dead; unique_ptr needs neither)
- close() contract documented once, on close()
- '^$_' identifier carve-out now inherited from the root .clang-tidy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jadecubes jadecubes self-assigned this Aug 16, 2026
@jadecubes
jadecubes changed the base branch from tooling/lint-ci-tests to main August 16, 2026 09:07
Debra and others added 8 commits August 16, 2026 17:18
Queue:
- Inline the can_enqueue/can_dequeue predicates: push's wait no longer
  re-tests closed_ inside the predicate, pop no longer evaluates the
  predicate twice per dequeue, and closure is checked exactly once per
  entry point
- close() skips both notify_all()s on repeat calls (std::exchange)
- Class doc states the thread-safety contract (STYLE.md requires it);
  drop the .ipp build-layout note from the published API docs

Tests:
- Shared tests/queue_test_util.hpp holds kSettleTime, run_blocked, and
  spawn_threads for both TUs (and future queue variants' tests)
- tests/.clang-tidy narrows identifier-length via Ignored*Names regexes
  instead of disabling the whole check

Benchmarks:
- One queue op per harness iteration: Google Benchmark regains control
  of run length (the 100k inner loop defeated --benchmark_min_time and
  made the CI smoke run execute ~500k ops), and the producer/consumer
  branch moves out of the timed region
- Guard odd ->Threads() counts with SkipWithError instead of a comment

CMake:
- cq INTERFACE links Threads::Threads (the library that creates the
  thread requirement owns it, not each consumer)
- gtest PRE_TEST discovery set project-wide instead of per-target

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uracy

Tests:
- run_blocked now asserts the operation was actually still blocked before
  unblocking (atomic finished flag); previously the four blocking tests
  passed even if the op returned immediately without blocking
- join_all helper (inline; the header is multi-TU) makes the stress
  test's shutdown ordering read as intent: join_all(producers); close();
  join_all(consumers)
- Stress producers assert once per thread instead of expanding an
  AssertionResult per item (25k per thread under TSan)
- tests/.clang-tidy ignore lists trimmed to names the code actually uses

Benchmarks (both defects found by measurement):
- SetItemsProcessed now reported from every thread; producer-only
  reporting inflated items/s by ~100000/threads (measured 1.33M/s
  reported vs 106 items/s actual at threads:8)
- setup prefills the ring half-full so measurement starts in steady
  state instead of on a condition-variable wakeup storm
- File header documents that thread spawn sits inside the timed region
  and recommends --benchmark_min_time=1s for meaningful numbers

CMake: comment accuracy fixes (garbled INTERFACE note, discovery-timeout
rationale, drop speculative sentence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- [[nodiscard]] on push/try_push/pop/try_pop — the operations whose
  discarded result is always a bug (reading out after an ignored
  try_pop, silent data loss after an ignored try_push); accessors
  already had it
- Class doc gains the two missing contracts STYLE.md requires: lifetime
  (close() + join before destruction; destroying with blocked threads
  is UB) and exception behavior (throwing T move-assign leaves the
  queue consistent)
- size()/closed() documented as advisory snapshots that must not drive
  control flow — the try_ operations are the atomic alternative
- try_pop doc states out is untouched on failure
- Bench prefill explicitly discards try_push's result with a cannot-fail
  note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… harness

Benchmarks (measured: reported scaling was wrong by a factor of threads):
- SetItemsProcessed(iterations * threads): Benchmark sums real time across
  threads, so per-thread iteration counts divided the true aggregate rate
  by the thread count (2->8 slowdown reported 6.9x, actually 1.9x)
- Single-thread roundtrip counts push+pop as two ops, making its unit
  identical to the threaded benchmark
- Odd-thread guard moved from a per-run runtime check to a static_assert
  beside the thread-count constants
- Prefill self-checks via one postcondition instead of 512 (void) casts

Tests:
- run_blocked rewritten on std::async/std::future: wait_for IS the
  still-blocked check and get() IS the join — drops the atomic flag, the
  memory orders, and the hand-rolled comment; return type now deduced
- kSettleTime 20ms -> 5ms (measured: the sleeps were 66% of the Debug
  suite wall time; 5ms is still ~250x the spawn-to-block latency)
- Stress producers report failures via ADD_FAILURE on the failure path
  only; [[nodiscard]] on both test helpers
- tests/.clang-tidy states the policy (check off) instead of allowlisting
  each identifier in use

Docs: dedupe the class comment (snapshot caveat said once at class level,
notification note moved beside the notify calls in the .ipp, lifetime
rationale said once); gtest discovery settings co-located on the single
gtest_discover_tests call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t harness

README (the published numbers were measured before round 3 changed the
benchmark's accounting, so every row and the story built on them was stale):
- Re-measured on this machine: 105.1M ops/s single-thread round trip,
  35.7M SPSC, 21.6M MPMC, with CV per row
- The 2->8 thread slowdown is 1.65x, not the ~6x claimed; the SPSC run's CV
  is 1.4%, not ~18%, so the lock-convoy-jitter reading is gone
- Define the unit once (an op is one push or one pop) and drop the
  'counted on the producer side only' claim, which the benchmark never did
- Note the measured cost of unconditional notify (~20% MPMC, ~26-37% SPSC)
  so v1 is not read as the fastest single-mutex design
- Lint commands note CI's version pin

Benchmarks:
- MinTime(1s) on the registrations: run length is a property of the
  benchmark, so the documented command reproduces the table (the ctest
  smoke run still overrides with 1x)
- Drop the file header's claim that thread spawn is inside the timed
  region — StartKeepRunning hits the start barrier before ResumeTiming

Tests:
- Delete join_all: spawn_threads already returns jthreads, whose
  destructor joins; the stress test says producers.clear() / consumers.clear()
- spawn_threads uses jthread's forwarding constructor instead of a wrapper
  lambda per thread
- Test helpers move to namespace cq::testing, out of the shipping namespace

CMake/lint:
- Sanitizer-vs-benchmark policy stated once, with a status message, instead
  of silently dropping benchmark targets from an explicit -DCQ_BUILD_BENCHMARKS=ON
- The 'auto _' identifier carve-out moves to bench/.clang-tidy beside the
  other two benchmark-loop exceptions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All four items are defects in the previous round's own changes:

- tests: rename cq::testing -> cq::test_util. Inside namespace cq, the old
  name shadowed GoogleTest's ::testing, so any future fixture written as
  ': public testing::Test' (or TestWithParam, or a gmock matcher import)
  would fail to compile; gtest's macros only survive it by expanding to
  fully-qualified ::testing
- README: the Per-op column mixed denominators — per round trip on row 1,
  per-thread on rows 2-3 — so it read as a 6.6x MPMC penalty, silently
  contradicting the 1.65x in the prose two lines below. Now 1/throughput
  throughout (9.5 / 28.0 / 46.3 ns), stated above the table
- bench: MinTime() on the registration beats --benchmark_min_time=<N>s
  (ComputeMinTime prefers a non-zero registered value), so that flag is
  accepted and ignored; only the Nx form overrides. Comment says so
- CMake: CQ_BUILD_BENCHMARKS defaults to ON, so the policy block fired on
  the README's own TSan command and could never detect an explicit -D as
  the comment claimed; set() also shadowed the cache entry. Replaced with
  cmake_dependent_option so the cache reports what was built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CMakeLists: the previous comment credited cmake_dependent_option with a
  cache-visibility benefit it does not provide. Verified: under TSan the
  default path writes no cache entry at all, and an explicit
  -DCQ_BUILD_BENCHMARKS=ON is demoted to CQ_BUILD_BENCHMARKS:INTERNAL=ON —
  recording ON while OFF was built. The STATUS line is the real signal
- README: 19.1 ns predated the column's redefinition as 1/throughput;
  1/105.1M ops/s makes the round trip 19.0 ns

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jadecubes
jadecubes merged commit 535a648 into main Aug 17, 2026
4 checks passed
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