Skip to content

Commit ad6eb22

Browse files
committed
Ensure that files are parsed/evaluated only once
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.
1 parent 47c16fc commit ad6eb22

File tree

2 files changed

+111
-73
lines changed

2 files changed

+111
-73
lines changed

src/libexpr/eval.cc

Lines changed: 102 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
#include <nlohmann/json.hpp>
4040
#include <boost/container/small_vector.hpp>
41+
#include <boost/unordered/concurrent_flat_map.hpp>
4142

4243
#include "nix/util/strings-inline.hh"
4344

@@ -192,6 +193,27 @@ static Symbol getName(const AttrName & name, EvalState & state, Env & env)
192193

193194
static constexpr size_t BASE_ENV_SIZE = 128;
194195

196+
struct EvalState::SrcToStore
197+
{
198+
boost::concurrent_flat_map<SourcePath, StorePath> inner;
199+
};
200+
201+
struct EvalState::ImportResolutionCache
202+
{
203+
boost::concurrent_flat_map<SourcePath, SourcePath> inner;
204+
};
205+
206+
struct EvalState::FileEvalCache
207+
{
208+
boost::concurrent_flat_map<
209+
SourcePath,
210+
Value *,
211+
std::hash<SourcePath>,
212+
std::equal_to<SourcePath>,
213+
traceable_allocator<std::pair<const SourcePath, Value *>>>
214+
inner;
215+
};
216+
195217
EvalState::EvalState(
196218
const LookupPath & lookupPathFromArguments,
197219
ref<Store> store,
@@ -264,6 +286,9 @@ EvalState::EvalState(
264286
, debugRepl(nullptr)
265287
, debugStop(false)
266288
, trylevel(0)
289+
, srcToStore(make_ref<SrcToStore>())
290+
, importResolutionCache(make_ref<ImportResolutionCache>())
291+
, fileEvalCache(make_ref<FileEvalCache>())
267292
, regexCache(makeRegexCache())
268293
#if NIX_USE_BOEHMGC
269294
, valueAllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
@@ -1031,63 +1056,85 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
10311056
return &v;
10321057
}
10331058

1034-
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
1059+
/**
1060+
* A helper `Expr` class to lets us parse and evaluate Nix expressions
1061+
* from a thunk, ensuring that every file is parsed/evaluated only
1062+
* once (via the thunk stored in `EvalState::fileEvalCache`).
1063+
*/
1064+
struct ExprParseFile : Expr
10351065
{
1036-
FileEvalCache::iterator i;
1037-
if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
1038-
v = i->second;
1039-
return;
1040-
}
1066+
SourcePath & path;
1067+
bool mustBeTrivial;
10411068

1042-
auto resolvedPath = resolveExprPath(path);
1043-
if ((i = fileEvalCache.find(resolvedPath)) != fileEvalCache.end()) {
1044-
v = i->second;
1045-
return;
1069+
ExprParseFile(SourcePath & path, bool mustBeTrivial)
1070+
: path(path)
1071+
, mustBeTrivial(mustBeTrivial)
1072+
{
10461073
}
10471074

1048-
printTalkative("evaluating file '%1%'", resolvedPath);
1049-
Expr * e = nullptr;
1075+
void eval(EvalState & state, Env & env, Value & v) override
1076+
{
1077+
printTalkative("evaluating file '%s'", path);
1078+
1079+
auto e = state.parseExprFromFile(path);
10501080

1051-
auto j = fileParseCache.find(resolvedPath);
1052-
if (j != fileParseCache.end())
1053-
e = j->second;
1081+
try {
1082+
auto dts =
1083+
state.debugRepl
1084+
? makeDebugTraceStacker(
1085+
state, *e, state.baseEnv, e->getPos(), "while evaluating the file '%s':", path.to_string())
1086+
: nullptr;
1087+
1088+
// Enforce that 'flake.nix' is a direct attrset, not a
1089+
// computation.
1090+
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
1091+
state.error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
1092+
1093+
state.eval(e, v);
1094+
} catch (Error & e) {
1095+
state.addErrorTrace(e, "while evaluating the file '%s':", path.to_string());
1096+
throw;
1097+
}
1098+
}
1099+
};
10541100

1055-
if (!e)
1056-
e = parseExprFromFile(resolvedPath);
1101+
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
1102+
{
1103+
auto resolvedPath = getConcurrent(importResolutionCache->inner, path);
10571104

1058-
fileParseCache.emplace(resolvedPath, e);
1105+
if (!resolvedPath) {
1106+
resolvedPath = resolveExprPath(path);
1107+
importResolutionCache->inner.emplace(path, *resolvedPath);
1108+
}
10591109

1060-
try {
1061-
auto dts = debugRepl ? makeDebugTraceStacker(
1062-
*this,
1063-
*e,
1064-
this->baseEnv,
1065-
e->getPos(),
1066-
"while evaluating the file '%1%':",
1067-
resolvedPath.to_string())
1068-
: nullptr;
1069-
1070-
// Enforce that 'flake.nix' is a direct attrset, not a
1071-
// computation.
1072-
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
1073-
error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
1074-
eval(e, v);
1075-
} catch (Error & e) {
1076-
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath.to_string());
1077-
throw;
1110+
if (auto v2 = getConcurrent(fileEvalCache->inner, *resolvedPath)) {
1111+
forceValue(**v2, noPos);
1112+
v = **v2;
1113+
return;
10781114
}
10791115

1080-
fileEvalCache.emplace(resolvedPath, v);
1081-
if (path != resolvedPath)
1082-
fileEvalCache.emplace(path, v);
1116+
Value * vExpr;
1117+
ExprParseFile expr{*resolvedPath, mustBeTrivial};
1118+
1119+
fileEvalCache->inner.try_emplace_and_cvisit(
1120+
*resolvedPath,
1121+
nullptr,
1122+
[&](auto & i) {
1123+
vExpr = allocValue();
1124+
vExpr->mkThunk(&baseEnv, &expr);
1125+
i.second = vExpr;
1126+
},
1127+
[&](auto & i) { vExpr = i.second; });
1128+
1129+
forceValue(*vExpr, noPos);
1130+
1131+
v = *vExpr;
10831132
}
10841133

10851134
void EvalState::resetFileCache()
10861135
{
1087-
fileEvalCache.clear();
1088-
fileEvalCache.rehash(0);
1089-
fileParseCache.clear();
1090-
fileParseCache.rehash(0);
1136+
fileEvalCache->inner.clear();
1137+
fileEvalCache->inner.rehash(0);
10911138
inputCache->clear();
10921139
}
10931140

@@ -2372,24 +2419,26 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
23722419
if (nix::isDerivation(path.path.abs()))
23732420
error<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();
23742421

2375-
std::optional<StorePath> dstPath;
2376-
if (!srcToStore.cvisit(path, [&dstPath](const auto & kv) { dstPath.emplace(kv.second); })) {
2377-
dstPath.emplace(fetchToStore(
2422+
auto dstPathCached = getConcurrent(srcToStore->inner, path);
2423+
2424+
auto dstPath = dstPathCached ? *dstPathCached : [&]() {
2425+
auto dstPath = fetchToStore(
23782426
fetchSettings,
23792427
*store,
23802428
path.resolveSymlinks(SymlinkResolution::Ancestors),
23812429
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
23822430
path.baseName(),
23832431
ContentAddressMethod::Raw::NixArchive,
23842432
nullptr,
2385-
repair));
2386-
allowPath(*dstPath);
2387-
srcToStore.try_emplace(path, *dstPath);
2388-
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(*dstPath));
2389-
}
2433+
repair);
2434+
allowPath(dstPath);
2435+
srcToStore->inner.try_emplace(path, dstPath);
2436+
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
2437+
return dstPath;
2438+
}();
23902439

2391-
context.insert(NixStringContextElem::Opaque{.path = *dstPath});
2392-
return *dstPath;
2440+
context.insert(NixStringContextElem::Opaque{.path = dstPath});
2441+
return dstPath;
23932442
}
23942443

23952444
SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)

src/libexpr/include/nix/expr/eval.hh

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS`
2121
#include "nix/expr/config.hh"
2222

23-
#include <boost/unordered/concurrent_flat_map.hpp>
2423
#include <boost/unordered/unordered_flat_map.hpp>
2524
#include <map>
2625
#include <optional>
@@ -412,31 +411,21 @@ private:
412411

413412
/* Cache for calls to addToStore(); maps source paths to the store
414413
paths. */
415-
boost::concurrent_flat_map<SourcePath, StorePath, std::hash<SourcePath>> srcToStore;
414+
struct SrcToStore;
415+
ref<SrcToStore> srcToStore;
416416

417417
/**
418-
* A cache from path names to parse trees.
418+
* A cache that maps paths to "resolved" paths for importing Nix
419+
* expressions, i.e. `/foo` to `/foo/default.nix`.
419420
*/
420-
typedef boost::unordered_flat_map<
421-
SourcePath,
422-
Expr *,
423-
std::hash<SourcePath>,
424-
std::equal_to<SourcePath>,
425-
traceable_allocator<std::pair<const SourcePath, Expr *>>>
426-
FileParseCache;
427-
FileParseCache fileParseCache;
421+
struct ImportResolutionCache;
422+
ref<ImportResolutionCache> importResolutionCache;
428423

429424
/**
430-
* A cache from path names to values.
425+
* A cache from resolved paths to values.
431426
*/
432-
typedef boost::unordered_flat_map<
433-
SourcePath,
434-
Value,
435-
std::hash<SourcePath>,
436-
std::equal_to<SourcePath>,
437-
traceable_allocator<std::pair<const SourcePath, Value>>>
438-
FileEvalCache;
439-
FileEvalCache fileEvalCache;
427+
struct FileEvalCache;
428+
ref<FileEvalCache> fileEvalCache;
440429

441430
/**
442431
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.

0 commit comments

Comments
 (0)