Skip to content

Commit 9e97eca

Browse files
committed
libexpr: Include derivation names in the call stack profile
This makes the profiler much more useful by actually distiguishing different derivations being evaluated. This does make the implementation a bit more convoluted, but I think it's worth it.
1 parent a76c76a commit 9e97eca

File tree

2 files changed

+68
-10
lines changed

2 files changed

+68
-10
lines changed

src/libexpr/eval-profiler.cc

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ struct FunctorFrameInfo
9595
auto operator<=>(const FunctorFrameInfo & rhs) const = default;
9696
};
9797

98+
struct DerivationStrictFrameInfo
99+
{
100+
PosIdx callPos = noPos;
101+
std::string drvName;
102+
std::ostream & symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const;
103+
auto operator<=>(const DerivationStrictFrameInfo & rhs) const = default;
104+
};
105+
98106
/** Fallback frame info. */
99107
struct GenericFrameInfo
100108
{
@@ -103,7 +111,8 @@ struct GenericFrameInfo
103111
auto operator<=>(const GenericFrameInfo & rhs) const = default;
104112
};
105113

106-
using FrameInfo = std::variant<LambdaFrameInfo, PrimOpFrameInfo, FunctorFrameInfo, GenericFrameInfo>;
114+
using FrameInfo =
115+
std::variant<LambdaFrameInfo, PrimOpFrameInfo, FunctorFrameInfo, DerivationStrictFrameInfo, GenericFrameInfo>;
107116
using FrameStack = std::vector<FrameInfo>;
108117

109118
/**
@@ -121,6 +130,8 @@ class SampleStack : public EvalProfiler
121130
return Hooks().set(preFunctionCall).set(postFunctionCall);
122131
}
123132

133+
FrameInfo getPrimOpFrameInfo(const PrimOp & primOp, std::span<Value *> args, PosIdx pos);
134+
124135
public:
125136
SampleStack(EvalState & state, std::filesystem::path profileFile, std::chrono::nanoseconds period)
126137
: state(state)
@@ -142,14 +153,13 @@ class SampleStack : public EvalProfiler
142153

143154
void maybeSaveProfile(std::chrono::time_point<std::chrono::high_resolution_clock> now);
144155
void saveProfile();
145-
FrameInfo getFrameInfoFromValueAndPos(const Value & v, PosIdx pos);
156+
FrameInfo getFrameInfoFromValueAndPos(const Value & v, std::span<Value *> args, PosIdx pos);
146157

147158
SampleStack(SampleStack &&) = default;
148159
SampleStack & operator=(SampleStack &&) = delete;
149160
SampleStack(const SampleStack &) = delete;
150161
SampleStack & operator=(const SampleStack &) = delete;
151162
~SampleStack();
152-
153163
private:
154164
/** Hold on to an instance of EvalState for symbolizing positions. */
155165
EvalState & state;
@@ -163,15 +173,41 @@ class SampleStack : public EvalProfiler
163173
PosCache posCache;
164174
};
165175

166-
FrameInfo SampleStack::getFrameInfoFromValueAndPos(const Value & v, PosIdx pos)
176+
FrameInfo SampleStack::getPrimOpFrameInfo(const PrimOp & primOp, std::span<Value *> args, PosIdx pos)
177+
{
178+
auto derivationInfo = [&]() -> std::optional<FrameInfo> {
179+
/* Here we rely a bit on the implementation details of libexpr/primops/derivation.nix
180+
and derivationStrict primop. This is not ideal, but is necessary for
181+
the usefulness of the profiler. This might actually affect the evaluation,
182+
but the cost shouldn't be that high as to make the traces entirely inaccurate. */
183+
if (primOp.name == "derivationStrict") {
184+
try {
185+
/* Error context strings don't actually matter, since we ignore all eval errors. */
186+
state.forceAttrs(*args[0], pos, "");
187+
auto attrs = args[0]->attrs();
188+
auto nameAttr = state.getAttr(state.sName, attrs, "");
189+
auto drvName = std::string(state.forceStringNoCtx(*nameAttr->value, pos, ""));
190+
return DerivationStrictFrameInfo{.callPos = pos, .drvName = std::move(drvName)};
191+
} catch (...) {
192+
/* Ignore all errors, since those will be diagnosed by the evaluator itself. */
193+
}
194+
}
195+
196+
return std::nullopt;
197+
}();
198+
199+
return derivationInfo.value_or(PrimOpFrameInfo{.expr = &primOp, .callPos = pos});
200+
}
201+
202+
FrameInfo SampleStack::getFrameInfoFromValueAndPos(const Value & v, std::span<Value *> args, PosIdx pos)
167203
{
168204
/* NOTE: No actual references to garbage collected values are not held in
169205
the profiler. */
170206
if (v.isLambda())
171207
return LambdaFrameInfo{.expr = v.payload.lambda.fun, .callPos = pos};
172-
else if (v.isPrimOp())
173-
return PrimOpFrameInfo{.expr = v.primOp(), .callPos = pos};
174-
else if (v.isPrimOpApp())
208+
else if (v.isPrimOp()) {
209+
return getPrimOpFrameInfo(*v.primOp(), args, pos);
210+
} else if (v.isPrimOpApp())
175211
/* Resolve primOp eagerly. Must not hold on to a reference to a Value. */
176212
return PrimOpFrameInfo{.expr = v.primOpAppPrimOp(), .callPos = pos};
177213
else if (state.isFunctor(v)) {
@@ -186,10 +222,10 @@ FrameInfo SampleStack::getFrameInfoFromValueAndPos(const Value & v, PosIdx pos)
186222
return GenericFrameInfo{.pos = pos};
187223
}
188224

189-
[[gnu::noinline]] void SampleStack::preFunctionCallHook(
190-
EvalState & state, const Value & v, [[maybe_unused]] std::span<Value *> args, const PosIdx pos)
225+
[[gnu::noinline]] void
226+
SampleStack::preFunctionCallHook(EvalState & state, const Value & v, std::span<Value *> args, const PosIdx pos)
191227
{
192-
stack.push_back(getFrameInfoFromValueAndPos(v, pos));
228+
stack.push_back(getFrameInfoFromValueAndPos(v, args, pos));
193229

194230
auto now = std::chrono::high_resolution_clock::now();
195231

@@ -246,6 +282,18 @@ std::ostream & PrimOpFrameInfo::symbolize(const EvalState & state, std::ostream
246282
return os;
247283
}
248284

285+
std::ostream &
286+
DerivationStrictFrameInfo::symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const
287+
{
288+
/* Sometimes callsite position can have an unresolved origin, which
289+
leads to confusing «none»:0 locations in the profile. */
290+
auto pos = posCache.lookup(callPos);
291+
if (!std::holds_alternative<std::monostate>(pos.origin))
292+
os << posCache.lookup(callPos) << ":";
293+
os << "primop derivationStrict:" << drvName;
294+
return os;
295+
}
296+
249297
void SampleStack::maybeSaveProfile(std::chrono::time_point<std::chrono::high_resolution_clock> now)
250298
{
251299
if (now - lastDump >= profileDumpInterval)

tests/functional/flamegraph-profiler.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,13 @@ expect_trace 'let f2 = (x: x); in f2 1 2' "
8989
expect_trace '1 2' "
9090
«string»:1:1 1
9191
"
92+
93+
# Derivation
94+
expect_trace 'builtins.derivationStrict { name = "somepackage"; }' "
95+
«string»:1:1:primop derivationStrict:somepackage 1
96+
"
97+
98+
# Derivation without name attr
99+
expect_trace 'builtins.derivationStrict { }' "
100+
«string»:1:1:primop derivationStrict 1
101+
"

0 commit comments

Comments
 (0)