Skip to content

Parallel eval performance improvements#194

Merged
edolstra merged 3 commits intomainfrom
parallel-performance-tweaks
Sep 5, 2025
Merged

Parallel eval performance improvements#194
edolstra merged 3 commits intomainfrom
parallel-performance-tweaks

Conversation

@edolstra
Copy link
Copy Markdown
Collaborator

@edolstra edolstra commented Sep 5, 2025

Motivation

Fixes some false sharing issues identified by perf c2c:

  • Put mutable state in Store / LocalStore in a separately allocated object to avoid putting read-only fields and mutable fields in the same cacheline.
  • Make MountedSourceAccessor use boost::concurrent_flat_map instead of Sync.

Context

Summary by CodeRabbit

  • Performance
    • Reduced lock contention in cache operations for snappier builds and queries.
    • Faster garbage collection responsiveness.
    • Concurrent mount lookups for better scalability under load.
  • Stability
    • More robust synchronization across store and GC paths, lowering risk of race conditions.
  • Compatibility
    • No changes to public APIs or user-facing commands.
  • Refactor
    • Internal restructuring of caches and state management to use dedicated synchronization and concurrent containers.

`perf c2c` shows a lot of cacheline conflicts between purely read-only
Store methods (like `parseStorePath()`) and the Sync classes. So
allocate pathInfoCache separately to avoid that.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 5, 2025

Walkthrough

Refactors internal synchronization and caching: migrates Store pathInfoCache to its own SharedSync ref, updates LocalStore state to ref<Sync> with pointer-style locking, adjusts GC locking calls, simplifies BinaryCacheStore cache update, replaces mounts map with boost concurrent_flat_map, and introduces hashing and type alias additions.

Changes

Cohort / File(s) Summary
Store pathInfoCache refactor
src/libstore/include/nix/store/store-api.hh, src/libstore/store-api.cc, src/libstore/remote-store.cc, src/libstore/binary-cache-store.cc
Replaces Store::state wrapper with ref<SharedSync<LRUCache<...>>> pathInfoCache; updates cache access to pathInfoCache->lock()/readLock(); clears/updates cache via its own lock; removes extra locking in BinaryCacheStore writeNarInfo.
LocalStore state locking changes
src/libstore/include/nix/store/local-store.hh, src/libstore/local-store.cc, src/libstore/gc.cc
Converts _state from Sync to ref<Sync>; all call sites use _state->lock(); adjusts GC code to lock via pointer; routes pathInfoCache ops through dedicated lock.
Mounted source accessor concurrency
src/libutil/mounted-source-accessor.cc
Replaces SharedSync<std::map<...>> with boost::concurrent_flat_map; initialization via mount(); lookups use cvisit; insertion via emplace; removes explicit read/write locking.
CanonPath hashing update
src/libutil/include/nix/util/canon-path.hh
Adds boost-based hash_value(CanonPath); updates friend decl; includes boost hash header; std::hash specialization delegates to hash_value.
Utility type aliases
src/libutil/include/nix/util/ref.hh, src/libutil/include/nix/util/sync.hh
Adds public using element_type = T; to ref and SyncBase for type-trait compatibility; no behavioral changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Store
  participant PathInfoCache as PathInfoCache (LRU, SharedSync)
  participant DiskCache as NarInfo DiskCache

  Client->>Store: queryPathInfo(key)
  Store->>PathInfoCache: lock().get(key)
  alt cache hit
    PathInfoCache-->>Store: PathInfo
    Store-->>Client: PathInfo
  else cache miss
    Store->>DiskCache: read(key)
    alt disk hit
      DiskCache-->>Store: PathInfo
      Store->>PathInfoCache: lock().upsert(key, value)
      Store-->>Client: PathInfo
    else disk miss
      Store-->>Client: not found
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • cole-h

Poem

A whisker twitch, a cache made neat,
I hop through locks with lighter feet.
Maps now race without a fret,
Boosted paths are firmly set.
With gentle thumps I sign and stash—
Hooray for threads that never clash! 🐇💾

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch parallel-performance-tweaks

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Sep 5, 2025

@github-actions github-actions bot temporarily deployed to pull request September 5, 2025 12:31 Inactive
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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libstore/store-api.cc (1)

466-471: Keep in-memory negative cache in sync with disk cache

When disk cache reports oInvalid (negative), you upsert a negative entry into pathInfoCache in queryPathInfoFromClientCache(), but not in isValidPath() when valid == false. Adding an in-memory negative upsert there avoids a second round-trip before the disk cache is consulted again.

Apply this diff in isValidPath() after computing valid:

     bool valid = isValidPathUncached(storePath);

     if (diskCache && !valid)
         // FIXME: handle valid = true case.
         diskCache->upsertNarInfo(
             config.getReference().render(/*FIXME withParams=*/false), std::string(storePath.hashPart()), 0);
+    if (!valid) {
+        pathInfoCache->lock()->upsert(storePath.to_string(), PathInfoCacheValue{});
+    }

Also applies to: 551-556

🧹 Nitpick comments (10)
src/libutil/include/nix/util/canon-path.hh (1)

263-264: Minor: mark hash_value noexcept

The hasher doesn’t throw; adding noexcept tightens the contract.

-    friend std::size_t hash_value(const CanonPath &);
+    friend std::size_t hash_value(const CanonPath &) noexcept;
@@
-inline std::size_t hash_value(const CanonPath & path)
+inline std::size_t hash_value(const CanonPath & path) noexcept
 {
     boost::hash<std::string_view> hasher;
     return hasher(path.path);
 }

Also applies to: 268-273

src/libstore/store-api.cc (3)

313-314: Constructor: ensure correct Sync wrapper construction and cache capacity type

Confirm that decltype(pathInfoCache)::element_type is the Sync wrapper type that forwards the (size_t) capacity to the underlying cache. If it is not forwarding args, the cache may be default-constructed, ignoring config.pathInfoCacheSize. If forwarding isn't guaranteed, prefer constructing the underlying cache explicitly via the Sync wrapper's factory or helper.


455-459: Client-side cache hit path looks good; minor perf tidy

LGTM on TTL-based early returns and stat accounting. To shave a few copies, consider precomputing auto key = storePath.to_string(); once per call and reuse it for get()/upsert() sites.

Also applies to: 538-545


814-815: Stats read is safe; consider exposing approximate size if lock-free needed later

Current readLock()->size() is fine. If this becomes a hot path, consider an approximate/atomic size to avoid reader lock contention.

src/libutil/mounted-source-accessor.cc (2)

65-68: getMount/resolve: verify ref-to-shared_ptr conversion and cvisit usage

  • getMount() returns std::shared_ptr<SourceAccessor> but assigns ref<SourceAccessor> into optional and later returns *res. Ensure ref<T> is implicitly convertible to std::shared_ptr<T>; otherwise, unwrap explicitly.
  • cvisit usage is fine; for clarity, capture by const auto & and return early when found.

Here’s a minimal, explicit return:

-        std::optional<ref<SourceAccessor>> res;
-        mounts.cvisit(mountPoint, [&](auto & x) { res = x.second; });
-        if (res)
-            return *res;
+        std::optional<ref<SourceAccessor>> res;
+        mounts.cvisit(mountPoint, [&](const auto & x) { res = x.second; });
+        if (res) {
+            // explicitly unwrap if implicit conversion is unavailable
+            return std::shared_ptr<SourceAccessor>(*res);
+        }

Also applies to: 87-95


16-20: Constructor: keep root mount invariant and avoid double-work

Asserting _mounts.contains(CanonPath::root) is good. Since you immediately call mount() in a loop, consider reserving in the concurrent map when available to reduce rehashing if _mounts is large.

src/libstore/local-store.cc (4)

432-441: Destructor: safe GC wait; minor nit on lock scope

Good pattern: capture gcFuture under lock, then wait without holding it. Nit: declare future inside the inner scope to narrow lifetime.


811-815: Lock churn: repeated _state->lock() inside tight loops

Multiple hot paths reacquire _state->lock() per call or per iteration (e.g., isValidPathUncached, queryAllValidPaths, queryValidDerivers, queryStaticPartialDerivationOutputMap, queryPathFromHashPart, registerValidPaths, invalidatePathChecked, addSignatures, queryRealisationUncached, verifyPath). Consider hoisting a single lock across each loop/transactional region to reduce mutex contention.

Example (pattern):

-    for (...) {
-        retrySQLite<void>([&]{ auto state(_state->lock()); ... });
-    }
+    retrySQLite<void>([&]{
+        auto state(_state->lock());
+        for (...) {
+            ...
+        }
+    });

Also applies to: 825-835, 845-849, 852-863, 868-879, 889-901, 953-1000, 1341-1357, 1566-1579, 1602-1631, 1527-1531


1441-1443: verifyStore: reuse single state lock for batched updates

Inside the content-check loop, each updatePathInfo(*_state->lock(), *info) re-locks. Cache a state lock once outside the loop to avoid repeated lock/unlock during DB updates.

Apply this small refactor:

-        for (auto & i : validPaths) {
+        auto state(_state->lock());
+        for (auto & i : validPaths) {
             ...
-            if (update)
-                updatePathInfo(*_state->lock(), *info);
+            if (update)
+                updatePathInfo(*state, *info);

1561-1562: VACUUM under lock (LGTM); consider exclusive DB phase warning

Running vacuum under the state lock is fine. If this is user-facing, consider a brief log line as it may block writers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7143558 and f5a0e14.

📒 Files selected for processing (11)
  • src/libstore/binary-cache-store.cc (1 hunks)
  • src/libstore/gc.cc (3 hunks)
  • src/libstore/include/nix/store/local-store.hh (1 hunks)
  • src/libstore/include/nix/store/store-api.hh (2 hunks)
  • src/libstore/local-store.cc (17 hunks)
  • src/libstore/remote-store.cc (1 hunks)
  • src/libstore/store-api.cc (6 hunks)
  • src/libutil/include/nix/util/canon-path.hh (2 hunks)
  • src/libutil/include/nix/util/ref.hh (1 hunks)
  • src/libutil/include/nix/util/sync.hh (1 hunks)
  • src/libutil/mounted-source-accessor.cc (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/libstore/binary-cache-store.cc (1)
src/libstore/include/nix/store/store-api.hh (1)
  • pathInfoCache (872-875)
src/libstore/remote-store.cc (1)
src/libstore/include/nix/store/store-api.hh (1)
  • pathInfoCache (872-875)
src/libstore/gc.cc (3)
src/libstore/daemon.cc (1)
  • state (111-122)
src/libstore/filetransfer.cc (1)
  • state (647-657)
src/libstore/http-binary-cache-store.cc (2)
  • state (96-105)
  • state (107-118)
src/libstore/local-store.cc (4)
src/libutil/include/nix/util/ref.hh (3)
  • make_ref (94-94)
  • make_ref (98-102)
  • make_ref (98-98)
src/libstore/include/nix/store/store-api.hh (3)
  • pathInfoCache (872-875)
  • info (535-539)
  • info (762-762)
src/libstore/include/nix/store/local-store.hh (32)
  • state (381-382)
  • state (384-384)
  • state (385-385)
  • state (409-409)
  • state (411-411)
  • state (415-415)
  • state (417-417)
  • state (419-419)
  • state (426-426)
  • state (428-428)
  • state (449-449)
  • state (450-450)
  • info (234-234)
  • info (357-357)
  • info (379-379)
  • info (380-380)
  • path (211-211)
  • path (217-218)
  • path (220-220)
  • path (222-222)
  • path (225-225)
  • path (245-245)
  • path (274-274)
  • path (295-298)
  • path (295-295)
  • path (305-305)
  • path (319-319)
  • path (393-399)
  • path (424-424)
  • path (431-431)
  • path (433-433)
  • path (444-444)
src/libstore/include/nix/store/sqlite.hh (2)
  • retrySQLite (202-213)
  • retrySQLite (202-202)
src/libstore/store-api.cc (2)
src/libutil/include/nix/util/ref.hh (3)
  • make_ref (94-94)
  • make_ref (98-102)
  • make_ref (98-98)
src/libstore/include/nix/store/store-api.hh (17)
  • pathInfoCache (872-875)
  • path (318-318)
  • path (340-340)
  • path (346-346)
  • path (353-353)
  • path (357-357)
  • path (394-394)
  • path (400-400)
  • path (405-405)
  • path (415-415)
  • path (451-451)
  • path (461-464)
  • path (461-461)
  • path (474-477)
  • path (474-474)
  • path (482-482)
  • path (490-490)
src/libutil/mounted-source-accessor.cc (2)
src/libutil/include/nix/util/canon-path.hh (26)
  • path (81-84)
  • path (86-89)
  • path (91-94)
  • path (106-109)
  • path (111-114)
  • path (261-261)
  • path (279-282)
  • path (279-279)
  • CanonPath (52-52)
  • CanonPath (54-57)
  • CanonPath (54-54)
  • CanonPath (62-65)
  • CanonPath (70-70)
  • CanonPath (79-79)
  • x (134-137)
  • x (134-134)
  • x (139-142)
  • x (139-139)
  • x (191-194)
  • x (191-191)
  • x (196-199)
  • x (196-196)
  • x (207-222)
  • x (207-207)
  • x (235-235)
  • x (240-240)
src/libutil/include/nix/util/mounted-source-accessor.hh (2)
  • mountPoint (9-9)
  • mountPoint (15-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build_aarch64-darwin / build
  • GitHub Check: build_x86_64-linux / build
🔇 Additional comments (14)
src/libutil/include/nix/util/sync.hh (1)

39-40: Expose element_type for pointer-like interoperability — looks good

This enables generic utilities to treat SyncBase like a pointer wrapper (e.g., traits, helpers). No behavioral change.

src/libutil/include/nix/util/ref.hh (1)

22-23: Add element_type alias to ref — consistent and useful

Matches SyncBase and improves compatibility with pointer-like generic code.

src/libstore/include/nix/store/local-store.hh (1)

177-177: _state construction and usage verified. _state is initialized via make_ref in the LocalStore constructor, there are no direct ._state accesses, all call sites use _state->lock(), and no reference cycles were detected.

src/libstore/binary-cache-store.cc (1)

128-130: Switch to fine-grained cache lock — appropriate

Using pathInfoCache->lock()->upsert(...) narrows the critical section and avoids unrelated global locking. Matches the new cache layout.

src/libstore/remote-store.cc (1)

767-767: Clear pathInfoCache under its own lock — aligned with new design

Localizes synchronization to the cache itself post-GC; consistent with other call sites.

src/libstore/gc.cc (1)

939-946: Pointer-style locking change is correct

Switching to _state->lock() aligns with _state being ref<Sync<State>>. The three call sites look consistent and safe.

Also applies to: 970-976, 987-988

src/libstore/include/nix/store/store-api.hh (3)

320-323: Good: pathInfoCache moved behind its own ref<SharedSync<...>> to reduce false sharing

This separation is a sensible design improvement for concurrency and cache locality.


872-875: clearPathInfoCache() updated correctly

Using pathInfoCache->lock()->clear() matches the new ownership/synchronization model.


320-323: No action needed: pathInfoCache is initialized with config.pathInfoCacheSize in the Store constructor.

src/libutil/include/nix/util/canon-path.hh (2)

11-12: Include boost container_hash is appropriate

Required for Boost’s ADL-based hash_value integration. Looks good.


279-282: Good: std::hash delegates to nix::hash_value

Unifies hashing semantics across std/boost containers.

src/libstore/store-api.cc (1)

592-593: Correctness: populate cache on successful uncached query (LGTM)

Upserting the positive result into pathInfoCache after an uncached fetch is correct and prevents redundant backend lookups.

src/libstore/local-store.cc (2)

121-129: State isolation: new ref<Sync> init looks good; ensure no hidden copies

_state(make_ref<Sync<State>>()) + immediate auto state(_state->lock()); is correct and prevents false sharing with other Store fields.

Also applies to: 129-131


720-722: Keep pathInfoCache coherent on local DB mutations (LGTM)

Upserting on addValidPath() and erasing on invalidatePath() keep the in-memory cache consistent with DB changes.

Also applies to: 1013-1014

#include "nix/util/mounted-source-accessor.hh"
#include "nix/util/sync.hh"

#include <boost/unordered/concurrent_flat_map.hpp>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Compile fix: wrong namespace for Boost concurrent_flat_map

The type is boost::unordered::concurrent_flat_map, not boost::concurrent_flat_map. Current code won’t compile.

Apply this diff:

-#include <boost/unordered/concurrent_flat_map.hpp>
+#include <boost/unordered/concurrent_flat_map.hpp>
@@
-    boost::concurrent_flat_map<CanonPath, ref<SourceAccessor>> mounts;
+    boost::unordered::concurrent_flat_map<CanonPath, ref<SourceAccessor>> mounts;

Also applies to: 9-9

🤖 Prompt for AI Agents
In src/libutil/mounted-source-accessor.cc around line 3 (and also the occurrence
at line 9), the code uses the wrong Boost namespace; change any uses of
boost::concurrent_flat_map to boost::unordered::concurrent_flat_map and keep the
include as <boost/unordered/concurrent_flat_map.hpp> so the type resolves
correctly and the file compiles.

Comment on lines 82 to 85
void mount(CanonPath mountPoint, ref<SourceAccessor> accessor) override
{
mounts_.lock()->insert_or_assign(std::move(mountPoint), accessor);
mounts.emplace(std::move(mountPoint), std::move(accessor));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Mount semantics regression: emplace won’t replace existing mounts

Previously insert_or_assign replaced an existing mount; emplace will silently keep the old one. If replacement is intended, use insert_or_assign (preferred) or erase(mountPoint) then emplace.

Apply one of:

-        mounts.emplace(std::move(mountPoint), std::move(accessor));
+        mounts.insert_or_assign(std::move(mountPoint), std::move(accessor));

or

+        mounts.erase(mountPoint);
         mounts.emplace(std::move(mountPoint), std::move(accessor));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void mount(CanonPath mountPoint, ref<SourceAccessor> accessor) override
{
mounts_.lock()->insert_or_assign(std::move(mountPoint), accessor);
mounts.emplace(std::move(mountPoint), std::move(accessor));
}
void mount(CanonPath mountPoint, ref<SourceAccessor> accessor) override
{
mounts.insert_or_assign(std::move(mountPoint), std::move(accessor));
}
🤖 Prompt for AI Agents
In src/libutil/mounted-source-accessor.cc around lines 82 to 85, the current
mount method uses mounts.emplace which does not replace an existing entry and
thus regresses previous semantics; change the call to
mounts.insert_or_assign(std::move(mountPoint), std::move(accessor)) to ensure
existing mounts are replaced (or alternatively explicitly erase(mountPoint)
before emplace if insert_or_assign is unavailable), preserving the move
semantics for both key and value.

@edolstra edolstra added this pull request to the merge queue Sep 5, 2025
Merged via the queue into main with commit 0a75d23 Sep 5, 2025
35 checks passed
@edolstra edolstra deleted the parallel-performance-tweaks branch September 5, 2025 13:51
@coderabbitai coderabbitai bot mentioned this pull request Nov 7, 2025
@coderabbitai coderabbitai bot mentioned this pull request Nov 27, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jan 9, 2026
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.

2 participants