Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 86 additions & 53 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

#include <nlohmann/json.hpp>
#include <boost/container/small_vector.hpp>
#include <boost/unordered/concurrent_flat_map.hpp>

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

Expand Down Expand Up @@ -264,6 +265,9 @@ EvalState::EvalState(
, debugRepl(nullptr)
, debugStop(false)
, trylevel(0)
, srcToStore(make_ref<decltype(srcToStore)::element_type>())
, importResolutionCache(make_ref<decltype(importResolutionCache)::element_type>())
, fileEvalCache(make_ref<decltype(fileEvalCache)::element_type>())
, regexCache(makeRegexCache())
#if NIX_USE_BOEHMGC
, valueAllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
Expand Down Expand Up @@ -1026,63 +1030,90 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
return &v;
}

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

auto resolvedPath = resolveExprPath(path);
if ((i = fileEvalCache.find(resolvedPath)) != fileEvalCache.end()) {
v = i->second;
return;
ExprParseFile(SourcePath & path, bool mustBeTrivial)
: path(path)
, mustBeTrivial(mustBeTrivial)
{
}

printTalkative("evaluating file '%1%'", resolvedPath);
Expr * e = nullptr;
void eval(EvalState & state, Env & env, Value & v) override
{
printTalkative("evaluating file '%s'", path);

auto e = state.parseExprFromFile(path);

auto j = fileParseCache.find(resolvedPath);
if (j != fileParseCache.end())
e = j->second;
try {
auto dts =
state.debugRepl
? makeDebugTraceStacker(
state, *e, state.baseEnv, e->getPos(), "while evaluating the file '%s':", path.to_string())
: nullptr;

// Enforce that 'flake.nix' is a direct attrset, not a
// computation.
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
state.error<EvalError>("file '%s' must be an attribute set", path).debugThrow();

state.eval(e, v);
} catch (Error & e) {
state.addErrorTrace(e, "while evaluating the file '%s':", path.to_string());
throw;
}
}
};

if (!e)
e = parseExprFromFile(resolvedPath);
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
{
auto resolvedPath = getConcurrent(*importResolutionCache, path);

fileParseCache.emplace(resolvedPath, e);
if (!resolvedPath) {
resolvedPath = resolveExprPath(path);
importResolutionCache->emplace(path, *resolvedPath);
}

try {
auto dts = debugRepl ? makeDebugTraceStacker(
*this,
*e,
this->baseEnv,
e->getPos(),
"while evaluating the file '%1%':",
resolvedPath.to_string())
: nullptr;

// Enforce that 'flake.nix' is a direct attrset, not a
// computation.
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
eval(e, v);
} catch (Error & e) {
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath.to_string());
throw;
if (auto v2 = getConcurrent(*fileEvalCache, *resolvedPath)) {
forceValue(**v2, noPos);
v = **v2;
return;
}

fileEvalCache.emplace(resolvedPath, v);
if (path != resolvedPath)
fileEvalCache.emplace(path, v);
Value * vExpr;
// FIXME: put ExprParseFile on the stack instead of the heap once
// https://github.com/NixOS/nix/pull/13930 is merged. That will ensure
// the post-condition that `expr` is unreachable after
// `forceValue()` returns.
auto expr = new ExprParseFile{*resolvedPath, mustBeTrivial};

fileEvalCache->try_emplace_and_cvisit(
*resolvedPath,
nullptr,
[&](auto & i) {
vExpr = allocValue();
vExpr->mkThunk(&baseEnv, expr);
i.second = vExpr;
},
[&](auto & i) { vExpr = i.second; });

forceValue(*vExpr, noPos);

v = *vExpr;
}

void EvalState::resetFileCache()
{
fileEvalCache.clear();
fileEvalCache.rehash(0);
fileParseCache.clear();
fileParseCache.rehash(0);
importResolutionCache->clear();
fileEvalCache->clear();
inputCache->clear();
}

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

std::optional<StorePath> dstPath;
if (!srcToStore.cvisit(path, [&dstPath](const auto & kv) { dstPath.emplace(kv.second); })) {
dstPath.emplace(fetchToStore(
auto dstPathCached = getConcurrent(*srcToStore, path);

auto dstPath = dstPathCached ? *dstPathCached : [&]() {
auto dstPath = fetchToStore(
fetchSettings,
*store,
path.resolveSymlinks(SymlinkResolution::Ancestors),
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
path.baseName(),
ContentAddressMethod::Raw::NixArchive,
nullptr,
repair));
allowPath(*dstPath);
srcToStore.try_emplace(path, *dstPath);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(*dstPath));
}
repair);
allowPath(dstPath);
srcToStore->try_emplace(path, dstPath);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
return dstPath;
}();

context.insert(NixStringContextElem::Opaque{.path = *dstPath});
return *dstPath;
context.insert(NixStringContextElem::Opaque{.path = dstPath});
return dstPath;
}

SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
Expand Down
30 changes: 12 additions & 18 deletions src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS`
#include "nix/expr/config.hh"

#include <boost/unordered/concurrent_flat_map.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/concurrent_flat_map_fwd.hpp>

#include <map>
#include <optional>
#include <functional>
Expand Down Expand Up @@ -403,37 +404,30 @@ private:

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

/**
* A cache from path names to parse trees.
* A cache that maps paths to "resolved" paths for importing Nix
* expressions, i.e. `/foo` to `/foo/default.nix`.
*/
typedef boost::unordered_flat_map<
SourcePath,
Expr *,
std::hash<SourcePath>,
std::equal_to<SourcePath>,
traceable_allocator<std::pair<const SourcePath, Expr *>>>
FileParseCache;
FileParseCache fileParseCache;
ref<boost::concurrent_flat_map<SourcePath, SourcePath>> importResolutionCache;

/**
* A cache from path names to values.
* A cache from resolved paths to values.
*/
typedef boost::unordered_flat_map<
ref<boost::concurrent_flat_map<
SourcePath,
Value,
Value *,
std::hash<SourcePath>,
std::equal_to<SourcePath>,
traceable_allocator<std::pair<const SourcePath, Value>>>
FileEvalCache;
FileEvalCache fileEvalCache;
traceable_allocator<std::pair<const SourcePath, Value *>>>>
fileEvalCache;

/**
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.
* Grouped by file.
*/
boost::unordered_flat_map<SourcePath, DocCommentMap, std::hash<SourcePath>> positionToDocComment;
boost::unordered_flat_map<SourcePath, DocCommentMap> positionToDocComment;

LookupPath lookupPath;

Expand Down
6 changes: 3 additions & 3 deletions src/libfetchers/filtering-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path)
struct AllowListSourceAccessorImpl : AllowListSourceAccessor
{
std::set<CanonPath> allowedPrefixes;
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> allowedPaths;
boost::unordered_flat_set<CanonPath> allowedPaths;

AllowListSourceAccessorImpl(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
boost::unordered_flat_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError)
: AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError))
, allowedPrefixes(std::move(allowedPrefixes))
Expand All @@ -86,7 +86,7 @@ struct AllowListSourceAccessorImpl : AllowListSourceAccessor
ref<AllowListSourceAccessor> AllowListSourceAccessor::create(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
boost::unordered_flat_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError)
{
return make_ref<AllowListSourceAccessorImpl>(
Expand Down
4 changes: 2 additions & 2 deletions src/libfetchers/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ struct GitSourceAccessor : SourceAccessor
return toHash(*git_tree_entry_id(entry));
}

boost::unordered_flat_map<CanonPath, TreeEntry, std::hash<CanonPath>> lookupCache;
boost::unordered_flat_map<CanonPath, TreeEntry> lookupCache;

/* Recursively look up 'path' relative to the root. */
git_tree_entry * lookup(State & state, const CanonPath & path)
Expand Down Expand Up @@ -1254,7 +1254,7 @@ GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllow
makeFSSourceAccessor(path),
std::set<CanonPath>{wd.files},
// Always allow access to the root, but not its children.
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>>{CanonPath::root},
boost::unordered_flat_set<CanonPath>{CanonPath::root},
std::move(makeNotAllowedError))
.cast<SourceAccessor>();
if (exportIgnore)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct AllowListSourceAccessor : public FilteringSourceAccessor
static ref<AllowListSourceAccessor> create(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
boost::unordered_flat_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError);

using FilteringSourceAccessor::FilteringSourceAccessor;
Expand Down
14 changes: 11 additions & 3 deletions src/libutil/include/nix/util/canon-path.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <set>
#include <vector>

#include <boost/container_hash/hash.hpp>

namespace nix {

/**
Expand Down Expand Up @@ -258,20 +260,26 @@ public:
*/
std::string makeRelative(const CanonPath & path) const;

friend struct std::hash<CanonPath>;
friend std::size_t hash_value(const CanonPath &);
};

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

inline std::size_t hash_value(const CanonPath & path)
{
boost::hash<std::string_view> hasher;
return hasher(path.path);
}

} // namespace nix

template<>
struct std::hash<nix::CanonPath>
{
using is_avalanching = std::true_type;

std::size_t operator()(const nix::CanonPath & s) const noexcept
std::size_t operator()(const nix::CanonPath & path) const noexcept
{
return std::hash<std::string>{}(s.path);
return nix::hash_value(path);
}
};
14 changes: 11 additions & 3 deletions src/libutil/include/nix/util/source-path.hh
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,23 @@ struct SourcePath

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

inline std::size_t hash_value(const SourcePath & path)
{
std::size_t hash = 0;
boost::hash_combine(hash, path.accessor->number);
boost::hash_combine(hash, path.path);
return hash;
}

} // namespace nix

template<>
struct std::hash<nix::SourcePath>
{
using is_avalanching = std::true_type;

std::size_t operator()(const nix::SourcePath & s) const noexcept
{
std::size_t hash = 0;
hash_combine(hash, s.accessor->number, s.path);
return hash;
return nix::hash_value(s);
}
};
11 changes: 11 additions & 0 deletions src/libutil/include/nix/util/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ typename T::mapped_type * get(T & map, const K & key)
template<class T, typename K>
typename T::mapped_type * get(T && map, const K & key) = delete;

/**
* Look up a value in a `boost::concurrent_flat_map`.
*/
template<class T>
std::optional<typename T::mapped_type> getConcurrent(const T & map, const typename T::key_type & key)
{
std::optional<typename T::mapped_type> res;
map.cvisit(key, [&](auto & x) { res = x.second; });
return res;
}

/**
* Get a value for the specified key from an associate container, or a default value if the key isn't present.
*/
Expand Down
4 changes: 1 addition & 3 deletions src/libutil/posix-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ std::optional<struct stat> PosixSourceAccessor::cachedLstat(const CanonPath & pa
// former is not hashable on libc++.
Path absPath = makeAbsPath(path).string();

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

auto st = nix::maybeLstat(absPath.c_str());
Expand Down
Loading