Ensure that files are parsed/evaluated only once#13938
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.
Pretty neat refactor. I like this a lot
| vExpr = allocValue(); | ||
| vExpr->mkThunk(&baseEnv, &expr); |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.