-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Ensure that files are parsed/evaluated only once #13938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@edolstra could you rebase? |
When doing multithreaded evaluation, we want to ensure that any Nix file is parsed and evaluated only once. The easiest way to do this is to rely on thunks, since those ensure locking in the multithreaded evaluator. `fileEvalCache` is now a mapping from `SourcePath` to a `Value *`. The value is initially a thunk (pointing to a `ExprParseFile` helper object) that can be forced to parse and evaluate the file. So a subsequent thread requesting the same file will see a thunk that is possibly locked and wait for it. The parser cache is gone since it's no longer needed. However, there is a new `importResolutionCache` that maps `SourcePath`s to `SourcePath`s (e.g. `/foo` to `/foo/default.nix`). Previously we put multiple entries in `fileEvalCache`, which was ugly and could result in work duplication.
947a480 to
8d8f49c
Compare
|
@Mic92 Done! |
xokdvium
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty neat refactor. I like this a lot
| vExpr = allocValue(); | ||
| vExpr->mkThunk(&baseEnv, &expr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason this has to be in a callback? Can't we just do this:
Value * vExpr = allocValue();
ExprParseFile expr{*resolvedPath, mustBeTrivial};
vExpr->mkThunk(&baseEnv, &expr);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It avoids an allocation in the case where the fileEvalCache entry already exists.
| if (path != resolvedPath) | ||
| fileEvalCache.emplace(path, v); | ||
| Value * vExpr; | ||
| ExprParseFile expr{*resolvedPath, mustBeTrivial}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a bug. This expression is allocated on the stack and it the results in a dangling pointer. E.g this leads to a segfault in:
nix eval --expr '(builtins.getFlake "github:nixos/nixpkgs/25.05")' --impure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, maybe there's something else at play here.
| fileParseCache.emplace(resolvedPath, e); | ||
| void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial) | ||
| { | ||
| auto resolvedPath = getConcurrent(*importResolutionCache, path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leads to a dangling reference, because this variable is allocated on the stack, but it's later saved to the heap allocated in *vExpr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the file evaluation caching mechanism to ensure that Nix files are parsed and evaluated only once, particularly in preparation for multithreaded evaluation. The implementation moves from a parser cache approach to a thunk-based system where files are represented as lazy-evaluated expressions.
Key changes:
- Replace separate parser and evaluation caches with a unified thunk-based file evaluation cache
- Introduce import resolution caching to map source paths to resolved paths
- Simplify hash template specializations by removing explicit hash type parameters
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libutil/posix-source-accessor.cc | Refactor cache lookup to use new getConcurrent utility function |
| src/libutil/include/nix/util/util.hh | Add getConcurrent helper for concurrent flat map lookups |
| src/libutil/include/nix/util/source-path.hh | Restructure hash implementation with Boost compatibility |
| src/libutil/include/nix/util/canon-path.hh | Restructure hash implementation with Boost compatibility |
| src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh | Remove explicit hash type parameter from unordered sets |
| src/libfetchers/git-utils.cc | Remove explicit hash type parameter from unordered maps |
| src/libfetchers/filtering-source-accessor.cc | Remove explicit hash type parameter from unordered sets |
| src/libexpr/include/nix/expr/eval.hh | Replace parser cache with import resolution cache and refactor file evaluation cache |
| src/libexpr/eval.cc | Implement new thunk-based file evaluation system with ExprParseFile helper |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| ExprParseFile expr{*resolvedPath, mustBeTrivial}; | ||
|
|
||
| fileEvalCache->try_emplace_and_cvisit( | ||
| *resolvedPath, | ||
| nullptr, | ||
| [&](auto & i) { | ||
| vExpr = allocValue(); | ||
| vExpr->mkThunk(&baseEnv, &expr); |
Copilot
AI
Sep 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ExprParseFile object is created on the stack but its address is passed to vExpr->mkThunk(). This creates a dangling reference when the stack object goes out of scope. The expression should be allocated on the heap or managed differently to ensure it remains valid for the lifetime of the thunk.
| ExprParseFile expr{*resolvedPath, mustBeTrivial}; | |
| fileEvalCache->try_emplace_and_cvisit( | |
| *resolvedPath, | |
| nullptr, | |
| [&](auto & i) { | |
| vExpr = allocValue(); | |
| vExpr->mkThunk(&baseEnv, &expr); | |
| auto expr = new ExprParseFile{*resolvedPath, mustBeTrivial}; | |
| fileEvalCache->try_emplace_and_cvisit( | |
| *resolvedPath, | |
| nullptr, | |
| [&](auto & i) { | |
| vExpr = allocValue(); | |
| vExpr->mkThunk(&baseEnv, expr); |
| SourcePath & path; | ||
| bool mustBeTrivial; | ||
|
|
||
| auto resolvedPath = resolveExprPath(path); | ||
| if ((i = fileEvalCache.find(resolvedPath)) != fileEvalCache.end()) { | ||
| v = i->second; | ||
| return; | ||
| ExprParseFile(SourcePath & path, bool mustBeTrivial) |
Copilot
AI
Sep 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing a reference to SourcePath in ExprParseFile is unsafe when the referenced object's lifetime is not guaranteed. Consider storing by value instead to avoid potential dangling references.
|
Revert in #14013. |
Revert "Merge pull request #13938 from NixOS/import-thunk"
Motivation
Extracted from the multithreaded evaluator.
When doing multithreaded evaluation, we want to ensure that any Nix file is parsed and evaluated only once. The easiest way to do this is to rely on thunks, since those ensure locking in the multithreaded evaluator (not part of this PR).
fileEvalCacheis now a mapping fromSourcePathto aValue *. The value is initially a thunk (pointing to aExprParseFilehelper object) that can be forced to parse and evaluate the file. So a subsequent thread requesting the same file will see a thunk that is possibly locked and wait for it.The parser cache is gone since it's no longer needed. However, there is a new
importResolutionCachethat mapsSourcePaths toSourcePaths (e.g./footo/foo/default.nix). Previously we put multiple entries infileEvalCache, which was ugly and could result in work duplication.Context
Add 👍 to pull requests you find important.
The Nix maintainer team uses a GitHub project board to schedule and track reviews.