From 6b4a86a9e337b3b90bb02c6fcb01a986e70da074 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Fri, 16 May 2025 21:53:06 +0000 Subject: [PATCH 1/2] libexpr: Add `EvalProfiler` This patch adds an EvalProfiler and MultiEvalProfiler that can be used to insert hooks into the evaluation for the purposes of function tracing (what function-trace currently does) or for flamegraph/tracy profilers. See the following commits for how this is supposed to be integrated into the evaluator and performance considerations. --- src/libexpr/eval-profiler.cc | 48 ++++++++ src/libexpr/include/nix/expr/eval-profiler.hh | 113 ++++++++++++++++++ .../include/nix/expr/function-trace.hh | 1 + src/libexpr/include/nix/expr/meson.build | 1 + src/libexpr/meson.build | 1 + 5 files changed, 164 insertions(+) create mode 100644 src/libexpr/eval-profiler.cc create mode 100644 src/libexpr/include/nix/expr/eval-profiler.hh diff --git a/src/libexpr/eval-profiler.cc b/src/libexpr/eval-profiler.cc new file mode 100644 index 000000000..5374d7d99 --- /dev/null +++ b/src/libexpr/eval-profiler.cc @@ -0,0 +1,48 @@ +#include "nix/expr/eval-profiler.hh" +#include "nix/expr/nixexpr.hh" + +namespace nix { + +void EvalProfiler::preFunctionCallHook( + const EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ +} + +void EvalProfiler::postFunctionCallHook( + const EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ +} + +void MultiEvalProfiler::preFunctionCallHook( + const EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ + for (auto & profiler : profilers) { + if (profiler->getNeededHooks().test(Hook::preFunctionCall)) + profiler->preFunctionCallHook(state, v, args, pos); + } +} + +void MultiEvalProfiler::postFunctionCallHook( + const EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ + for (auto & profiler : profilers) { + if (profiler->getNeededHooks().test(Hook::postFunctionCall)) + profiler->postFunctionCallHook(state, v, args, pos); + } +} + +EvalProfiler::Hooks MultiEvalProfiler::getNeededHooksImpl() const +{ + Hooks hooks; + for (auto & p : profilers) + hooks |= p->getNeededHooks(); + return hooks; +} + +void MultiEvalProfiler::addProfiler(ref profiler) +{ + profilers.push_back(profiler); + invalidateNeededHooks(); +} + +} diff --git a/src/libexpr/include/nix/expr/eval-profiler.hh b/src/libexpr/include/nix/expr/eval-profiler.hh new file mode 100644 index 000000000..763b737f7 --- /dev/null +++ b/src/libexpr/include/nix/expr/eval-profiler.hh @@ -0,0 +1,113 @@ +#pragma once +/** + * @file + * + * Evaluation profiler interface definitions and builtin implementations. + */ + +#include "nix/util/ref.hh" + +#include +#include +#include +#include + +namespace nix { + +class EvalState; +class PosIdx; +struct Value; + +class EvalProfiler +{ +public: + enum Hook { + preFunctionCall, + postFunctionCall, + }; + + static constexpr std::size_t numHooks = Hook::postFunctionCall + 1; + using Hooks = std::bitset; + +private: + std::optional neededHooks; + +protected: + /** Invalidate the cached neededHooks. */ + void invalidateNeededHooks() + { + neededHooks = std::nullopt; + } + + /** + * Get which hooks need to be called. + * + * This is the actual implementation which has to be defined by subclasses. + * Public API goes through the needsHooks, which is a + * non-virtual interface (NVI) which caches the return value. + */ + virtual Hooks getNeededHooksImpl() const + { + return Hooks{}; + } + +public: + /** + * Hook called in the EvalState::callFunction preamble. + * Gets called only if (getNeededHooks().test(Hook::preFunctionCall)) is true. + * + * @param state Evaluator state. + * @param v Function being invoked. + * @param args Function arguments. + * @param pos Function position. + */ + virtual void + preFunctionCallHook(const EvalState & state, const Value & v, std::span args, const PosIdx pos); + + /** + * Hook called on EvalState::callFunction exit. + * Gets called only if (getNeededHooks().test(Hook::postFunctionCall)) is true. + * + * @param state Evaluator state. + * @param v Function being invoked. + * @param args Function arguments. + * @param pos Function position. + */ + virtual void + postFunctionCallHook(const EvalState & state, const Value & v, std::span args, const PosIdx pos); + + virtual ~EvalProfiler() = default; + + /** + * Get which hooks need to be invoked for this EvalProfiler instance. + */ + Hooks getNeededHooks() + { + if (neededHooks.has_value()) + return *neededHooks; + return *(neededHooks = getNeededHooksImpl()); + } +}; + +/** + * Profiler that invokes multiple profilers at once. + */ +class MultiEvalProfiler : public EvalProfiler +{ + std::vector> profilers; + + [[gnu::noinline]] Hooks getNeededHooksImpl() const override; + +public: + MultiEvalProfiler() = default; + + /** Register a profiler instance. */ + void addProfiler(ref profiler); + + [[gnu::noinline]] void + preFunctionCallHook(const EvalState & state, const Value & v, std::span args, const PosIdx pos) override; + [[gnu::noinline]] void + postFunctionCallHook(const EvalState & state, const Value & v, std::span args, const PosIdx pos) override; +}; + +} diff --git a/src/libexpr/include/nix/expr/function-trace.hh b/src/libexpr/include/nix/expr/function-trace.hh index dc92d4b5c..f9b82d8a5 100644 --- a/src/libexpr/include/nix/expr/function-trace.hh +++ b/src/libexpr/include/nix/expr/function-trace.hh @@ -2,6 +2,7 @@ ///@file #include "nix/expr/eval.hh" +#include "nix/expr/eval-profiler.hh" #include diff --git a/src/libexpr/include/nix/expr/meson.build b/src/libexpr/include/nix/expr/meson.build index 50ea8f3c2..db902a616 100644 --- a/src/libexpr/include/nix/expr/meson.build +++ b/src/libexpr/include/nix/expr/meson.build @@ -14,6 +14,7 @@ headers = [config_pub_h] + files( 'eval-error.hh', 'eval-gc.hh', 'eval-inline.hh', + 'eval-profiler.hh', 'eval-settings.hh', 'eval.hh', 'function-trace.hh', diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build index 2b465b85a..dc50d2b19 100644 --- a/src/libexpr/meson.build +++ b/src/libexpr/meson.build @@ -140,6 +140,7 @@ sources = files( 'eval-cache.cc', 'eval-error.cc', 'eval-gc.cc', + 'eval-profiler.cc', 'eval-settings.cc', 'eval.cc', 'function-trace.cc', From fa6f69f9c5b2d753b7199161ab40ec661f0c190f Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Fri, 16 May 2025 22:29:18 +0000 Subject: [PATCH 2/2] libexpr: Use `EvalProfiler` for `FunctionCallTrace` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This wires up the {pre,post}FunctionCallHook machinery in EvalState::callFunction and migrates FunctionCallTrace to use the new EvalProfiler mechanisms for tracing. Note that branches when the hook gets called are marked with [[unlikely]] as a hint to the compiler that this is not a hot path. For non-tracing evaluation this should be a 100% predictable branch, so the performance cost is nonexistent. Some measurements to prove support this point: ``` nix build .#nix-cli nix build github:nixos/nix/d692729759e4e370361cc5105fbeb0e33137ca9e#nix-cli --out-link before ``` (Before) ``` $ taskset -c 2,3 hyperfine "GC_INITIAL_HEAP_SIZE=16g before/bin/nix eval nixpkgs#gnome --no-eval-cache" --warmup 4 Benchmark 1: GC_INITIAL_HEAP_SIZE=16g before/bin/nix eval nixpkgs#gnome --no-eval-cache Time (mean ± σ): 2.517 s ± 0.032 s [User: 1.464 s, System: 0.476 s] Range (min … max): 2.464 s … 2.557 s 10 runs ``` (After) ``` $ taskset -c 2,3 hyperfine "GC_INITIAL_HEAP_SIZE=16g result/bin/nix eval nixpkgs#gnome --no-eval-cache" --warmup 4 Benchmark 1: GC_INITIAL_HEAP_SIZE=16g result/bin/nix eval nixpkgs#gnome --no-eval-cache Time (mean ± σ): 2.499 s ± 0.022 s [User: 1.448 s, System: 0.478 s] Range (min … max): 2.472 s … 2.537 s 10 runs ``` --- src/libexpr/eval.cc | 15 +++++++++++--- src/libexpr/function-trace.cc | 12 +++++++---- src/libexpr/include/nix/expr/eval.hh | 4 ++++ .../include/nix/expr/function-trace.hh | 20 +++++++++++++------ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index ab7f33d5f..c171caa99 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -372,6 +372,10 @@ EvalState::EvalState( ); createBaseEnv(settings); + + /* Register function call tracer. */ + if (settings.traceFunctionCalls) + profiler.addProfiler(make_ref()); } @@ -1526,9 +1530,14 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, { auto _level = addCallDepth(pos); - auto trace = settings.traceFunctionCalls - ? std::make_unique(positions[pos]) - : nullptr; + auto neededHooks = profiler.getNeededHooks(); + if (neededHooks.test(EvalProfiler::preFunctionCall)) [[unlikely]] + profiler.preFunctionCallHook(*this, fun, args, pos); + + Finally traceExit_{[&](){ + if (profiler.getNeededHooks().test(EvalProfiler::postFunctionCall)) [[unlikely]] + profiler.postFunctionCallHook(*this, fun, args, pos); + }}; forceValue(fun, pos); diff --git a/src/libexpr/function-trace.cc b/src/libexpr/function-trace.cc index 1dce51726..993dd72d7 100644 --- a/src/libexpr/function-trace.cc +++ b/src/libexpr/function-trace.cc @@ -3,16 +3,20 @@ namespace nix { -FunctionCallTrace::FunctionCallTrace(const Pos & pos) : pos(pos) { +void FunctionCallTrace::preFunctionCallHook( + const EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ auto duration = std::chrono::high_resolution_clock::now().time_since_epoch(); auto ns = std::chrono::duration_cast(duration); - printMsg(lvlInfo, "function-trace entered %1% at %2%", pos, ns.count()); + printMsg(lvlInfo, "function-trace entered %1% at %2%", state.positions[pos], ns.count()); } -FunctionCallTrace::~FunctionCallTrace() { +void FunctionCallTrace::postFunctionCallHook( + const EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ auto duration = std::chrono::high_resolution_clock::now().time_since_epoch(); auto ns = std::chrono::duration_cast(duration); - printMsg(lvlInfo, "function-trace exited %1% at %2%", pos, ns.count()); + printMsg(lvlInfo, "function-trace exited %1% at %2%", state.positions[pos], ns.count()); } } diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index 6a6959bd8..ffbc84bcd 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -3,6 +3,7 @@ #include "nix/expr/attr-set.hh" #include "nix/expr/eval-error.hh" +#include "nix/expr/eval-profiler.hh" #include "nix/util/types.hh" #include "nix/expr/value.hh" #include "nix/expr/nixexpr.hh" @@ -903,6 +904,9 @@ private: typedef std::map FunctionCalls; FunctionCalls functionCalls; + /** Evaluation/call profiler. */ + MultiEvalProfiler profiler; + void incrFunctionCall(ExprLambda * fun); typedef std::map AttrSelects; diff --git a/src/libexpr/include/nix/expr/function-trace.hh b/src/libexpr/include/nix/expr/function-trace.hh index f9b82d8a5..9187cac63 100644 --- a/src/libexpr/include/nix/expr/function-trace.hh +++ b/src/libexpr/include/nix/expr/function-trace.hh @@ -4,14 +4,22 @@ #include "nix/expr/eval.hh" #include "nix/expr/eval-profiler.hh" -#include - namespace nix { -struct FunctionCallTrace +class FunctionCallTrace : public EvalProfiler { - const Pos pos; - FunctionCallTrace(const Pos & pos); - ~FunctionCallTrace(); + Hooks getNeededHooksImpl() const override + { + return Hooks().set(preFunctionCall).set(postFunctionCall); + } + +public: + FunctionCallTrace() = default; + + [[gnu::noinline]] void + preFunctionCallHook(const EvalState & state, const Value & v, std::span args, const PosIdx pos) override; + [[gnu::noinline]] void + postFunctionCallHook(const EvalState & state, const Value & v, std::span args, const PosIdx pos) override; }; + }