Skip to content

Commit 169a368

Browse files
authored
Merge pull request #14040 from NixOS/import-thunk
Ensure that files are parsed/evaluated only once (2nd attempt)
2 parents a0103fc + d32d77f commit 169a368

File tree

9 files changed

+138
-86
lines changed

9 files changed

+138
-86
lines changed

src/libexpr/eval.cc

Lines changed: 86 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

@@ -264,6 +265,9 @@ EvalState::EvalState(
264265
, debugRepl(nullptr)
265266
, debugStop(false)
266267
, trylevel(0)
268+
, srcToStore(make_ref<decltype(srcToStore)::element_type>())
269+
, importResolutionCache(make_ref<decltype(importResolutionCache)::element_type>())
270+
, fileEvalCache(make_ref<decltype(fileEvalCache)::element_type>())
267271
, regexCache(makeRegexCache())
268272
#if NIX_USE_BOEHMGC
269273
, valueAllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
@@ -1022,63 +1026,90 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
10221026
return &v;
10231027
}
10241028

1025-
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
1029+
/**
1030+
* A helper `Expr` class to lets us parse and evaluate Nix expressions
1031+
* from a thunk, ensuring that every file is parsed/evaluated only
1032+
* once (via the thunk stored in `EvalState::fileEvalCache`).
1033+
*/
1034+
struct ExprParseFile : Expr, gc
10261035
{
1027-
FileEvalCache::iterator i;
1028-
if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
1029-
v = i->second;
1030-
return;
1031-
}
1036+
// FIXME: make this a reference (see below).
1037+
SourcePath path;
1038+
bool mustBeTrivial;
10321039

1033-
auto resolvedPath = resolveExprPath(path);
1034-
if ((i = fileEvalCache.find(resolvedPath)) != fileEvalCache.end()) {
1035-
v = i->second;
1036-
return;
1040+
ExprParseFile(SourcePath & path, bool mustBeTrivial)
1041+
: path(path)
1042+
, mustBeTrivial(mustBeTrivial)
1043+
{
10371044
}
10381045

1039-
printTalkative("evaluating file '%1%'", resolvedPath);
1040-
Expr * e = nullptr;
1046+
void eval(EvalState & state, Env & env, Value & v) override
1047+
{
1048+
printTalkative("evaluating file '%s'", path);
1049+
1050+
auto e = state.parseExprFromFile(path);
10411051

1042-
auto j = fileParseCache.find(resolvedPath);
1043-
if (j != fileParseCache.end())
1044-
e = j->second;
1052+
try {
1053+
auto dts =
1054+
state.debugRepl
1055+
? makeDebugTraceStacker(
1056+
state, *e, state.baseEnv, e->getPos(), "while evaluating the file '%s':", path.to_string())
1057+
: nullptr;
1058+
1059+
// Enforce that 'flake.nix' is a direct attrset, not a
1060+
// computation.
1061+
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
1062+
state.error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
1063+
1064+
state.eval(e, v);
1065+
} catch (Error & e) {
1066+
state.addErrorTrace(e, "while evaluating the file '%s':", path.to_string());
1067+
throw;
1068+
}
1069+
}
1070+
};
10451071

1046-
if (!e)
1047-
e = parseExprFromFile(resolvedPath);
1072+
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
1073+
{
1074+
auto resolvedPath = getConcurrent(*importResolutionCache, path);
10481075

1049-
fileParseCache.emplace(resolvedPath, e);
1076+
if (!resolvedPath) {
1077+
resolvedPath = resolveExprPath(path);
1078+
importResolutionCache->emplace(path, *resolvedPath);
1079+
}
10501080

1051-
try {
1052-
auto dts = debugRepl ? makeDebugTraceStacker(
1053-
*this,
1054-
*e,
1055-
this->baseEnv,
1056-
e->getPos(),
1057-
"while evaluating the file '%1%':",
1058-
resolvedPath.to_string())
1059-
: nullptr;
1060-
1061-
// Enforce that 'flake.nix' is a direct attrset, not a
1062-
// computation.
1063-
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
1064-
error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
1065-
eval(e, v);
1066-
} catch (Error & e) {
1067-
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath.to_string());
1068-
throw;
1081+
if (auto v2 = getConcurrent(*fileEvalCache, *resolvedPath)) {
1082+
forceValue(**v2, noPos);
1083+
v = **v2;
1084+
return;
10691085
}
10701086

1071-
fileEvalCache.emplace(resolvedPath, v);
1072-
if (path != resolvedPath)
1073-
fileEvalCache.emplace(path, v);
1087+
Value * vExpr;
1088+
// FIXME: put ExprParseFile on the stack instead of the heap once
1089+
// https://github.com/NixOS/nix/pull/13930 is merged. That will ensure
1090+
// the post-condition that `expr` is unreachable after
1091+
// `forceValue()` returns.
1092+
auto expr = new ExprParseFile{*resolvedPath, mustBeTrivial};
1093+
1094+
fileEvalCache->try_emplace_and_cvisit(
1095+
*resolvedPath,
1096+
nullptr,
1097+
[&](auto & i) {
1098+
vExpr = allocValue();
1099+
vExpr->mkThunk(&baseEnv, expr);
1100+
i.second = vExpr;
1101+
},
1102+
[&](auto & i) { vExpr = i.second; });
1103+
1104+
forceValue(*vExpr, noPos);
1105+
1106+
v = *vExpr;
10741107
}
10751108

10761109
void EvalState::resetFileCache()
10771110
{
1078-
fileEvalCache.clear();
1079-
fileEvalCache.rehash(0);
1080-
fileParseCache.clear();
1081-
fileParseCache.rehash(0);
1111+
importResolutionCache->clear();
1112+
fileEvalCache->clear();
10821113
inputCache->clear();
10831114
}
10841115

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

2400-
std::optional<StorePath> dstPath;
2401-
if (!srcToStore.cvisit(path, [&dstPath](const auto & kv) { dstPath.emplace(kv.second); })) {
2402-
dstPath.emplace(fetchToStore(
2431+
auto dstPathCached = getConcurrent(*srcToStore, path);
2432+
2433+
auto dstPath = dstPathCached ? *dstPathCached : [&]() {
2434+
auto dstPath = fetchToStore(
24032435
fetchSettings,
24042436
*store,
24052437
path.resolveSymlinks(SymlinkResolution::Ancestors),
24062438
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
24072439
path.baseName(),
24082440
ContentAddressMethod::Raw::NixArchive,
24092441
nullptr,
2410-
repair));
2411-
allowPath(*dstPath);
2412-
srcToStore.try_emplace(path, *dstPath);
2413-
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(*dstPath));
2414-
}
2442+
repair);
2443+
allowPath(dstPath);
2444+
srcToStore->try_emplace(path, dstPath);
2445+
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
2446+
return dstPath;
2447+
}();
24152448

2416-
context.insert(NixStringContextElem::Opaque{.path = *dstPath});
2417-
return *dstPath;
2449+
context.insert(NixStringContextElem::Opaque{.path = dstPath});
2450+
return dstPath;
24182451
}
24192452

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

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

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
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>
24+
#include <boost/unordered/concurrent_flat_map_fwd.hpp>
25+
2526
#include <map>
2627
#include <optional>
2728
#include <functional>
@@ -403,37 +404,30 @@ private:
403404

404405
/* Cache for calls to addToStore(); maps source paths to the store
405406
paths. */
406-
boost::concurrent_flat_map<SourcePath, StorePath, std::hash<SourcePath>> srcToStore;
407+
ref<boost::concurrent_flat_map<SourcePath, StorePath>> srcToStore;
407408

408409
/**
409-
* A cache from path names to parse trees.
410+
* A cache that maps paths to "resolved" paths for importing Nix
411+
* expressions, i.e. `/foo` to `/foo/default.nix`.
410412
*/
411-
typedef boost::unordered_flat_map<
412-
SourcePath,
413-
Expr *,
414-
std::hash<SourcePath>,
415-
std::equal_to<SourcePath>,
416-
traceable_allocator<std::pair<const SourcePath, Expr *>>>
417-
FileParseCache;
418-
FileParseCache fileParseCache;
413+
ref<boost::concurrent_flat_map<SourcePath, SourcePath>> importResolutionCache;
419414

420415
/**
421-
* A cache from path names to values.
416+
* A cache from resolved paths to values.
422417
*/
423-
typedef boost::unordered_flat_map<
418+
ref<boost::concurrent_flat_map<
424419
SourcePath,
425-
Value,
420+
Value *,
426421
std::hash<SourcePath>,
427422
std::equal_to<SourcePath>,
428-
traceable_allocator<std::pair<const SourcePath, Value>>>
429-
FileEvalCache;
430-
FileEvalCache fileEvalCache;
423+
traceable_allocator<std::pair<const SourcePath, Value *>>>>
424+
fileEvalCache;
431425

432426
/**
433427
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.
434428
* Grouped by file.
435429
*/
436-
boost::unordered_flat_map<SourcePath, DocCommentMap, std::hash<SourcePath>> positionToDocComment;
430+
boost::unordered_flat_map<SourcePath, DocCommentMap> positionToDocComment;
437431

438432
LookupPath lookupPath;
439433

src/libfetchers/filtering-source-accessor.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path)
5959
struct AllowListSourceAccessorImpl : AllowListSourceAccessor
6060
{
6161
std::set<CanonPath> allowedPrefixes;
62-
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> allowedPaths;
62+
boost::unordered_flat_set<CanonPath> allowedPaths;
6363

6464
AllowListSourceAccessorImpl(
6565
ref<SourceAccessor> next,
6666
std::set<CanonPath> && allowedPrefixes,
67-
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
67+
boost::unordered_flat_set<CanonPath> && allowedPaths,
6868
MakeNotAllowedError && makeNotAllowedError)
6969
: AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError))
7070
, allowedPrefixes(std::move(allowedPrefixes))
@@ -86,7 +86,7 @@ struct AllowListSourceAccessorImpl : AllowListSourceAccessor
8686
ref<AllowListSourceAccessor> AllowListSourceAccessor::create(
8787
ref<SourceAccessor> next,
8888
std::set<CanonPath> && allowedPrefixes,
89-
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
89+
boost::unordered_flat_set<CanonPath> && allowedPaths,
9090
MakeNotAllowedError && makeNotAllowedError)
9191
{
9292
return make_ref<AllowListSourceAccessorImpl>(

src/libfetchers/git-utils.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ struct GitSourceAccessor : SourceAccessor
817817
return toHash(*git_tree_entry_id(entry));
818818
}
819819

820-
boost::unordered_flat_map<CanonPath, TreeEntry, std::hash<CanonPath>> lookupCache;
820+
boost::unordered_flat_map<CanonPath, TreeEntry> lookupCache;
821821

822822
/* Recursively look up 'path' relative to the root. */
823823
git_tree_entry * lookup(State & state, const CanonPath & path)
@@ -1254,7 +1254,7 @@ GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllow
12541254
makeFSSourceAccessor(path),
12551255
std::set<CanonPath>{wd.files},
12561256
// Always allow access to the root, but not its children.
1257-
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>>{CanonPath::root},
1257+
boost::unordered_flat_set<CanonPath>{CanonPath::root},
12581258
std::move(makeNotAllowedError))
12591259
.cast<SourceAccessor>();
12601260
if (exportIgnore)

src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct AllowListSourceAccessor : public FilteringSourceAccessor
7272
static ref<AllowListSourceAccessor> create(
7373
ref<SourceAccessor> next,
7474
std::set<CanonPath> && allowedPrefixes,
75-
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
75+
boost::unordered_flat_set<CanonPath> && allowedPaths,
7676
MakeNotAllowedError && makeNotAllowedError);
7777

7878
using FilteringSourceAccessor::FilteringSourceAccessor;

src/libutil/include/nix/util/canon-path.hh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#include <set>
99
#include <vector>
1010

11+
#include <boost/container_hash/hash.hpp>
12+
1113
namespace nix {
1214

1315
/**
@@ -258,20 +260,26 @@ public:
258260
*/
259261
std::string makeRelative(const CanonPath & path) const;
260262

261-
friend struct std::hash<CanonPath>;
263+
friend std::size_t hash_value(const CanonPath &);
262264
};
263265

264266
std::ostream & operator<<(std::ostream & stream, const CanonPath & path);
265267

268+
inline std::size_t hash_value(const CanonPath & path)
269+
{
270+
boost::hash<std::string_view> hasher;
271+
return hasher(path.path);
272+
}
273+
266274
} // namespace nix
267275

268276
template<>
269277
struct std::hash<nix::CanonPath>
270278
{
271279
using is_avalanching = std::true_type;
272280

273-
std::size_t operator()(const nix::CanonPath & s) const noexcept
281+
std::size_t operator()(const nix::CanonPath & path) const noexcept
274282
{
275-
return std::hash<std::string>{}(s.path);
283+
return nix::hash_value(path);
276284
}
277285
};

src/libutil/include/nix/util/source-path.hh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,23 @@ struct SourcePath
119119

120120
std::ostream & operator<<(std::ostream & str, const SourcePath & path);
121121

122+
inline std::size_t hash_value(const SourcePath & path)
123+
{
124+
std::size_t hash = 0;
125+
boost::hash_combine(hash, path.accessor->number);
126+
boost::hash_combine(hash, path.path);
127+
return hash;
128+
}
129+
122130
} // namespace nix
123131

124132
template<>
125133
struct std::hash<nix::SourcePath>
126134
{
135+
using is_avalanching = std::true_type;
136+
127137
std::size_t operator()(const nix::SourcePath & s) const noexcept
128138
{
129-
std::size_t hash = 0;
130-
hash_combine(hash, s.accessor->number, s.path);
131-
return hash;
139+
return nix::hash_value(s);
132140
}
133141
};

src/libutil/include/nix/util/util.hh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,17 @@ typename T::mapped_type * get(T & map, const K & key)
220220
template<class T, typename K>
221221
typename T::mapped_type * get(T && map, const K & key) = delete;
222222

223+
/**
224+
* Look up a value in a `boost::concurrent_flat_map`.
225+
*/
226+
template<class T>
227+
std::optional<typename T::mapped_type> getConcurrent(const T & map, const typename T::key_type & key)
228+
{
229+
std::optional<typename T::mapped_type> res;
230+
map.cvisit(key, [&](auto & x) { res = x.second; });
231+
return res;
232+
}
233+
223234
/**
224235
* Get a value for the specified key from an associate container, or a default value if the key isn't present.
225236
*/

src/libutil/posix-source-accessor.cc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,7 @@ std::optional<struct stat> PosixSourceAccessor::cachedLstat(const CanonPath & pa
9595
// former is not hashable on libc++.
9696
Path absPath = makeAbsPath(path).string();
9797

98-
std::optional<Cache::mapped_type> res;
99-
cache.cvisit(absPath, [&](auto & x) { res.emplace(x.second); });
100-
if (res)
98+
if (auto res = getConcurrent(cache, absPath))
10199
return *res;
102100

103101
auto st = nix::maybeLstat(absPath.c_str());

0 commit comments

Comments
 (0)