8000 perf: fix backend mutex serialisation in CommittedFactLoaderImpl::resolve (#252) by adityamukho · Pull Request #279 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

perf: fix backend mutex serialisation in CommittedFactLoaderImpl::resolve (#252) - #279

Merged
adityamukho merged 1 commit into
mainfrom
fix/252-btree-mutex
May 19, 2026
Merged

adityamukho merged 1 commit into
mainfrom
fix/252-btree-mutex

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Summary

  • Root cause: CommittedFactLoaderImpl::resolve acquired Arc<Mutex<B>> before calling page_cache.get_or_load(), so even warm-cache hits (which never call read_page) serialised all concurrent readers on one mutex
  • Trigger: daa80a9 (selective B+tree lookup, perf(post-1.0): B+Tree selective lookup for bound patterns #208/feat: selective B+Tree lookup in filter_facts_for_query (#208) #246) caused the query [:e0 :val ?v] to call get_facts_by_attribute(\":val\") returning all n FactRefs, each needing a resolve() call — 10k/100k mutex acquisitions vs the previous single stream_all() acquisition
  • Fix: promote MutexStorageBackend to pub(crate) and use it in resolve(), matching the pattern already used by OnDiskIndexReader::range_scan_* — backend mutex is now held only for actual cold-page reads

Benchmark results (local, fixed vs regressed CI)

Benchmark Regressed (CI 2026-05-16) Fixed
10k/2t 22ms / 90 e/s 22ms / 90 e/s
10k/4t 45ms / 89 e/s 31ms / 127 e/s (+43%)
10k/8t 70ms / 113 e/s 57ms / 139 e/s (+23%)
100k/2t 483ms / 4.1 e/s 388ms / 5.1 e/s (+24%)
100k/8t 2.1s / 3.8 e/s 1.48s / 5.4 e/s (+42%, now correctly above 100k/4t)

Test plan

  • All 962 tests pass (pre-push hook)
  • cargo clippy clean
  • cargo fmt --check clean
  • concurrent_btree_scan benchmarks show improved throughput scaling with thread count

Closes #252

🤖 Generated with Claude Code

…erImpl::resolve

CommittedFactLoaderImpl::resolve was acquiring the Arc<Mutex<B>> backend lock
before calling page_cache.get_or_load(), so even cache hits (which never call
read_page) serialised concurrent readers on the mutex. Under concurrent load
(e.g. the concurrent_btree_scan benchmark), all threads queued behind one
another even when every page was already warm in the LRU cache.

Fix: use MutexStorageBackend (promoted to pub(crate)) so the backend mutex is
acquired only if get_or_load has a cache miss and actually calls read_page —
identical to the pattern already used by OnDiskIndexReader::range_scan_*.

Result on concurrent_btree_scan (local, post-fix vs CI regression):
  10k/4t: 45ms → 31ms (+43% throughput)
  10k/8t: 70ms → 57ms (+23% throughput)
  100k/8t: 2.1s → 1.48s (+30% throughput, now correctly above 100k/4t)

Closes #252

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@adityamukho
adityamukho merged commit 5a71350 into main May 19, 2026
14 checks passed
@adityamukho
adityamukho deleted the fix/252-btree-mutex branch May 19, 2026 12:47
@codecov
codecov Bot commented May 19, 2026
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

adityamukho added a commit that referenced this pull request May 21, 2026
…minate per-resolve() Arc::clone (#280) (#281)

* perf: pre-build MutexStorageBackend in CommittedFactLoaderImpl to fix #280 regression

#279 fixed concurrent mutex serialization by switching resolve() to use
MutexStorageBackend instead of locking backend directly before get_or_load().
This kept #279's correctness (mutex only acquired on cache misses) but
introduced an Arc::clone + Arc::drop on every resolve() call.

At 10k facts, point_attribute queries call resolve() ~10k times per
iteration, making the per-call overhead measurable: +22% on
query/point_attribute/10k (29.4 ms vs 24.1 ms pre-regression), with
cascading slowdowns of 8-17% on other large-dataset query benchmarks.

Fix: construct MutexStorageBackend once at CommittedFactLoaderImpl
creation time and store it as a field. resolve() now uses
&self.backend_adapter directly — zero per-call allocation or atomic ops.
The concurrency property is fully preserved: backend mutex is still only
acquired inside MutexStorageBackend::read_page() on a cache miss.

Closes #280

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: apply rustfmt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
adityamukho added a commit that referenced this pull request May 21, 2026
- BENCHMARKS.md: replace stale v0.8.0 query latency entries for
  point_attribute and join_3pattern with v1.1.1 Bencher CI baseline
  (ubuntu-latest); add predicate_pushdown section; update regex_filter
  estimates to actual CI numbers; fix stale prose claiming selective
  B+tree lookup is a post-1.0 backlog item (shipped in #208); add
  Post-1.0 Updates header documenting #208/#279/#281 impact on numbers
- README.md: fix stale "(Phase 7 target: predicate pushdown)" note —
  predicate pushdown shipped in #207

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Benchmark regression - 2026-05-16

1 participant

0