Conversation
`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.
WalkthroughRefactors 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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 cacheWhen disk cache reports
oInvalid(negative), you upsert a negative entry intopathInfoCacheinqueryPathInfoFromClientCache(), but not inisValidPath()whenvalid == 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 computingvalid: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 noexceptThe hasher doesn’t throw; adding
noexcepttightens 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 typeConfirm that
decltype(pathInfoCache)::element_typeis 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, ignoringconfig.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 tidyLGTM 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 forget()/upsert()sites.Also applies to: 538-545
814-815: Stats read is safe; consider exposing approximate size if lock-free needed laterCurrent
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()returnsstd::shared_ptr<SourceAccessor>but assignsref<SourceAccessor>intooptionaland later returns*res. Ensureref<T>is implicitly convertible tostd::shared_ptr<T>; otherwise, unwrap explicitly.cvisitusage is fine; for clarity, capture byconst 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-workAsserting
_mounts.contains(CanonPath::root)is good. Since you immediately callmount()in a loop, consider reserving in the concurrent map when available to reduce rehashing if_mountsis large.src/libstore/local-store.cc (4)
432-441: Destructor: safe GC wait; minor nit on lock scopeGood pattern: capture
gcFutureunder lock, then wait without holding it. Nit: declarefutureinside the inner scope to narrow lifetime.
811-815: Lock churn: repeated_state->lock()inside tight loopsMultiple 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 updatesInside the content-check loop, each
updatePathInfo(*_state->lock(), *info)re-locks. Cache astatelock 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 warningRunning
vacuumunder 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.
📒 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 goodThis 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 usefulMatches 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 — appropriateUsing 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 designLocalizes synchronization to the cache itself post-GC; consistent with other call sites.
src/libstore/gc.cc (1)
939-946: Pointer-style locking change is correctSwitching to
_state->lock()aligns with_statebeingref<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 sharingThis separation is a sensible design improvement for concurrency and cache locality.
872-875: clearPathInfoCache() updated correctlyUsing
pathInfoCache->lock()->clear()matches the new ownership/synchronization model.
320-323: No action needed:pathInfoCacheis initialized withconfig.pathInfoCacheSizein the Store constructor.src/libutil/include/nix/util/canon-path.hh (2)
11-12: Include boost container_hash is appropriateRequired for Boost’s ADL-based
hash_valueintegration. Looks good.
279-282: Good: std::hash delegates to nix::hash_valueUnifies 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
pathInfoCacheafter 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>>())+ immediateauto state(_state->lock());is correct and prevents false sharing with otherStorefields.Also applies to: 129-131
720-722: Keep pathInfoCache coherent on local DB mutations (LGTM)Upserting on
addValidPath()and erasing oninvalidatePath()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> |
There was a problem hiding this comment.
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.
| 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)); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
Motivation
Fixes some false sharing issues identified by
perf c2c:Store/LocalStorein a separately allocated object to avoid putting read-only fields and mutable fields in the same cacheline.MountedSourceAccessoruseboost::concurrent_flat_mapinstead ofSync.Context
Summary by CodeRabbit