libexpr: detect cycles when converting values to JSON/XML#15888
libexpr: detect cycles when converting values to JSON/XML#15888dyegoaurelio wants to merge 3 commits into
Conversation
| } | ||
| const void * key = v.attrs(); | ||
| if (!seen.insert(key).second) | ||
| state.error<InfiniteRecursionError>("infinite recursion encountered while converting value to JSON") |
There was a problem hiding this comment.
Could add a bit more detail referring to the cyclic nature.
(and then do the same for nList)
| state.error<InfiniteRecursionError>("infinite recursion encountered while converting value to JSON") | |
| state.error<InfiniteRecursionError>("infinite recursion encountered while converting Nix value to JSON, because only cycle-free data can be represented") |
There was a problem hiding this comment.
I'm ok removing it from this PR and I don't use this feature myself.
But IMO the current behavior is much worse on the XML dump than on the json side.
nix-instantiate --xml --eval --strict --expr 'let a = { b = a; }; in a' just starts an infinite loop
There was a problem hiding this comment.
Failing eagerly would make more sense. Changing a diverging computation -> early error is perfectly safe.
There was a problem hiding this comment.
diverging computation -> early error is perfectly safe.
I think I agree actually. The old behavior was not usable, so compatibility is not much of a concern.
120c51d to
f4b2fbc
Compare
| const void * key = v.attrs(); | ||
| auto [it, fresh] = seen.try_emplace(key, currentPath ? *currentPath : ValuePath{}); | ||
| if (!fresh) | ||
| cycleError(key, it->second); | ||
| Finally cleanup([&] { seen.erase(key); }); | ||
| if (auto i = v.attrs()->get(state.s.outPath)) { | ||
| if (currentPath) | ||
| currentPath->emplace_back(state.s.outPath); | ||
| Finally popOutPath([&] { | ||
| if (currentPath) | ||
| currentPath->pop_back(); | ||
| }); | ||
| return valueToJSON(state, strict, *i->value, i->pos, context, copyToStore, seen, currentPath); | ||
| } else { | ||
| out = json::object(); | ||
| for (auto & a : v.attrs()->lexicographicOrder(state.symbols)) { | ||
| if (currentPath) | ||
| currentPath->emplace_back(a->name); | ||
| Finally popSegment([&] { | ||
| if (currentPath) | ||
| currentPath->pop_back(); | ||
| }); | ||
| try { | ||
| out.emplace( | ||
| state.symbols[a->name], | ||
| printValueAsJSON(state, strict, *a->value, a->pos, context, copyToStore)); | ||
| valueToJSON(state, strict, *a->value, a->pos, context, copyToStore, seen, currentPath)); |
There was a problem hiding this comment.
This does quite a bit more work in the happy (non-error) path. Does this measurably affect performance?
There was a problem hiding this comment.
I've just ran some benchs and it did add a 2-4% slowdown on ""realistic"" inputs and ~30% slowdown on deeply nested arrays/attrsets ([ [ [ [ [ 0 ] ] ] ] ])
Bench results
➜ builddir (915772aa5) ✗ ./src/libexpr-tests/nix-expr-benchmarks --benchmark_filter='BM_PrintValueAsJSON_.*' --benchmark_repetitions=10 --benchmark_report_aggregates_only=true
2026-05-23T14:15:19-03:00
Running ./src/libexpr-tests/nix-expr-benchmarks
Run on (12 X 5407.65 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x6)
L1 Instruction 32 KiB (x6)
L2 Unified 1024 KiB (x6)
L3 Unified 32768 KiB (x1)
Load Average: 1.24, 2.25, 2.94
--------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations UserCounters...
--------------------------------------------------------------------------------------------------------
BM_PrintValueAsJSON_DeepAttrs/100_mean 10355 ns 10346 ns 10 items_per_second=9.66602M/s
BM_PrintValueAsJSON_DeepAttrs/100_median 10358 ns 10348 ns 10 items_per_second=9.66331M/s
BM_PrintValueAsJSON_DeepAttrs/100_stddev 19.2 ns 19.5 ns 10 items_per_second=18.228k/s
BM_PrintValueAsJSON_DeepAttrs/100_cv 0.19 % 0.19 % 10 items_per_second=0.19%
BM_PrintValueAsJSON_DeepAttrs/1000_mean 107731 ns 107596 ns 10 items_per_second=9.2942M/s
BM_PrintValueAsJSON_DeepAttrs/1000_median 107626 ns 107499 ns 10 items_per_second=9.30239M/s
BM_PrintValueAsJSON_DeepAttrs/1000_stddev 433 ns 425 ns 10 items_per_second=36.6456k/s
BM_PrintValueAsJSON_DeepAttrs/1000_cv 0.40 % 0.39 % 10 items_per_second=0.39%
BM_PrintValueAsJSON_DeepAttrs/5000_mean 565658 ns 564697 ns 10 items_per_second=8.85452M/s
BM_PrintValueAsJSON_DeepAttrs/5000_median 565417 ns 564423 ns 10 items_per_second=8.8586M/s
BM_PrintValueAsJSON_DeepAttrs/5000_stddev 2890 ns 2969 ns 10 items_per_second=46.4683k/s
BM_PrintValueAsJSON_DeepAttrs/5000_cv 0.51 % 0.53 % 10 items_per_second=0.52%
BM_PrintValueAsJSON_DeepLists/100_mean 7726 ns 7717 ns 10 items_per_second=12.9599M/s
BM_PrintValueAsJSON_DeepLists/100_median 7711 ns 7700 ns 10 items_per_second=12.9864M/s
BM_PrintValueAsJSON_DeepLists/100_stddev 63.1 ns 63.3 ns 10 items_per_second=105.394k/s
BM_PrintValueAsJSON_DeepLists/100_cv 0.82 % 0.82 % 10 items_per_second=0.81%
BM_PrintValueAsJSON_DeepLists/1000_mean 81499 ns 81415 ns 10 items_per_second=12.2829M/s
BM_PrintValueAsJSON_DeepLists/1000_median 81429 ns 81350 ns 10 items_per_second=12.2926M/s
BM_PrintValueAsJSON_DeepLists/1000_stddev 154 ns 149 ns 10 items_per_second=22.3901k/s
BM_PrintValueAsJSON_DeepLists/1000_cv 0.19 % 0.18 % 10 items_per_second=0.18%
BM_PrintValueAsJSON_DeepLists/5000_mean 468968 ns 468349 ns 10 items_per_second=10.6762M/s
BM_PrintValueAsJSON_DeepLists/5000_median 468764 ns 468179 ns 10 items_per_second=10.6797M/s
BM_PrintValueAsJSON_DeepLists/5000_stddev 2934 ns 2935 ns 10 items_per_second=66.8169k/s
BM_PrintValueAsJSON_DeepLists/5000_cv 0.63 % 0.63 % 10 items_per_second=0.63%
BM_PrintValueAsJSON_BalancedAttrs/4/6_mean 505421 ns 504739 ns 10 items_per_second=10.8198M/s
BM_PrintValueAsJSON_BalancedAttrs/4/6_median 503841 ns 503196 ns 10 items_per_second=10.8526M/s
BM_PrintValueAsJSON_BalancedAttrs/4/6_stddev 3245 ns 3200 ns 10 items_per_second=68.3479k/s
BM_PrintValueAsJSON_BalancedAttrs/4/6_cv 0.64 % 0.63 % 10 items_per_second=0.63%
BM_PrintValueAsJSON_BalancedAttrs/8/4_mean 463897 ns 463382 ns 10 items_per_second=10.102M/s
BM_PrintValueAsJSON_BalancedAttrs/8/4_median 463920 ns 463355 ns 10 items_per_second=10.1024M/s
BM_PrintValueAsJSON_BalancedAttrs/8/4_stddev 2314 ns 2287 ns 10 items_per_second=49.4949k/s
BM_PrintValueAsJSON_BalancedAttrs/8/4_cv 0.50 % 0.49 % 10 items_per_second=0.49%
BM_PrintValueAsJSON_BalancedAttrs/16/3_mean 473908 ns 473439 ns 10 items_per_second=9.22856M/s
BM_PrintValueAsJSON_BalancedAttrs/16/3_median 473676 ns 473227 ns 10 items_per_second=9.23237M/s
BM_PrintValueAsJSON_BalancedAttrs/16/3_stddev 3089 ns 3051 ns 10 items_per_second=59.2301k/s
BM_PrintValueAsJSON_BalancedAttrs/16/3_cv 0.65 % 0.64 % 10 items_per_second=0.64%
BM_PrintValueAsJSON_PackageSet/1000_mean 260759 ns 260438 ns 10 items_per_second=3.83973M/s
BM_PrintValueAsJSON_PackageSet/1000_median 260360 ns 260038 ns 10 items_per_second=3.8456M/s
BM_PrintValueAsJSON_PackageSet/1000_stddev 851 ns 838 ns 10 items_per_second=12.3212k/s
BM_PrintValueAsJSON_PackageSet/1000_cv 0.33 % 0.32 % 10 items_per_second=0.32%
BM_PrintValueAsJSON_PackageSet/5000_mean 1552765 ns 1550252 ns 10 items_per_second=3.22592M/s
BM_PrintValueAsJSON_PackageSet/5000_median 1547737 ns 1545242 ns 10 items_per_second=3.23574M/s
BM_PrintValueAsJSON_PackageSet/5000_stddev 23391 ns 23080 ns 10 items_per_second=47.3982k/s
BM_PrintValueAsJSON_PackageSet/5000_cv 1.51 % 1.49 % 10 items_per_second=1.47%
BM_PrintValueAsJSON_PackageSet/10000_mean 3297638 ns 3292583 ns 10 items_per_second=3.03736M/s
BM_PrintValueAsJSON_PackageSet/10000_median 3282901 ns 3277862 ns 10 items_per_second=3.05077M/s
BM_PrintValueAsJSON_PackageSet/10000_stddev 30951 ns 30691 ns 10 items_per_second=28.1163k/s
BM_PrintValueAsJSON_PackageSet/10000_cv 0.94 % 0.93 % 10 items_per_second=0.93%
➜ builddir (infinite-recursion-json-decoding) ✗ ./src/libexpr-tests/nix-expr-benchmarks --benchmark_filter='BM_PrintValueAsJSON_.*' --benchmark_repetitions=10 --benchmark_report_aggregates_only=true
2026-05-23T14:17:28-03:00
Running ./src/libexpr-tests/nix-expr-benchmarks
Run on (12 X 5345.19 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x6)
L1 Instruction 32 KiB (x6)
L2 Unified 1024 KiB (x6)
L3 Unified 32768 KiB (x1)
Load Average: 1.59, 2.07, 2.78
--------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations UserCounters...
--------------------------------------------------------------------------------------------------------
BM_PrintValueAsJSON_DeepAttrs/100_mean 13692 ns 13669 ns 10 items_per_second=7.31601M/s
BM_PrintValueAsJSON_DeepAttrs/100_median 13730 ns 13707 ns 10 items_per_second=7.29581M/s
BM_PrintValueAsJSON_DeepAttrs/100_stddev 92.7 ns 92.4 ns 10 items_per_second=49.8798k/s
BM_PrintValueAsJSON_DeepAttrs/100_cv 0.68 % 0.68 % 10 items_per_second=0.68%
BM_PrintValueAsJSON_DeepAttrs/1000_mean 145453 ns 145191 ns 10 items_per_second=6.88756M/s
BM_PrintValueAsJSON_DeepAttrs/1000_median 145331 ns 145073 ns 10 items_per_second=6.89307M/s
BM_PrintValueAsJSON_DeepAttrs/1000_stddev 516 ns 503 ns 10 items_per_second=23.7781k/s
BM_PrintValueAsJSON_DeepAttrs/1000_cv 0.35 % 0.35 % 10 items_per_second=0.35%
BM_PrintValueAsJSON_DeepAttrs/5000_mean 751088 ns 749646 ns 10 items_per_second=6.67011M/s
BM_PrintValueAsJSON_DeepAttrs/5000_median 750557 ns 749179 ns 10 items_per_second=6.67398M/s
BM_PrintValueAsJSON_DeepAttrs/5000_stddev 5454 ns 5300 ns 10 items_per_second=46.6692k/s
BM_PrintValueAsJSON_DeepAttrs/5000_cv 0.73 % 0.71 % 10 items_per_second=0.70%
BM_PrintValueAsJSON_DeepLists/100_mean 9832 ns 9819 ns 10 items_per_second=10.185M/s
BM_PrintValueAsJSON_DeepLists/100_median 9775 ns 9763 ns 10 items_per_second=10.2431M/s
BM_PrintValueAsJSON_DeepLists/100_stddev 98.4 ns 97.0 ns 10 items_per_second=100.158k/s
BM_PrintValueAsJSON_DeepLists/100_cv 1.00 % 0.99 % 10 items_per_second=0.98%
BM_PrintValueAsJSON_DeepLists/1000_mean 108991 ns 108830 ns 10 items_per_second=9.18864M/s
BM_PrintValueAsJSON_DeepLists/1000_median 109030 ns 108864 ns 10 items_per_second=9.18579M/s
BM_PrintValueAsJSON_DeepLists/1000_stddev 194 ns 205 ns 10 items_per_second=17.2999k/s
BM_PrintValueAsJSON_DeepLists/1000_cv 0.18 % 0.19 % 10 items_per_second=0.19%
BM_PrintValueAsJSON_DeepLists/5000_mean 545378 ns 544444 ns 10 items_per_second=9.18371M/s
BM_PrintValueAsJSON_DeepLists/5000_median 545477 ns 544448 ns 10 items_per_second=9.18361M/s
BM_PrintValueAsJSON_DeepLists/5000_stddev 860 ns 866 ns 10 items_per_second=14.6199k/s
BM_PrintValueAsJSON_DeepLists/5000_cv 0.16 % 0.16 % 10 items_per_second=0.16%
BM_PrintValueAsJSON_BalancedAttrs/4/6_mean 520316 ns 519477 ns 10 items_per_second=10.5127M/s
BM_PrintValueAsJSON_BalancedAttrs/4/6_median 520534 ns 519572 ns 10 items_per_second=10.5106M/s
BM_PrintValueAsJSON_BalancedAttrs/4/6_stddev 2453 ns 2364 ns 10 items_per_second=47.774k/s
BM_PrintValueAsJSON_BalancedAttrs/4/6_cv 0.47 % 0.46 % 10 items_per_second=0.45%
BM_PrintValueAsJSON_BalancedAttrs/8/4_mean 484706 ns 483517 ns 10 items_per_second=9.68868M/s
BM_PrintValueAsJSON_BalancedAttrs/8/4_median 478137 ns 477374 ns 10 items_per_second=9.80589M/s
BM_PrintValueAsJSON_BalancedAttrs/8/4_stddev 14991 ns 14346 ns 10 items_per_second=282.474k/s
BM_PrintValueAsJSON_BalancedAttrs/8/4_cv 3.09 % 2.97 % 10 items_per_second=2.92%
BM_PrintValueAsJSON_BalancedAttrs/16/3_mean 486369 ns 485096 ns 10 items_per_second=9.00762M/s
BM_PrintValueAsJSON_BalancedAttrs/16/3_median 485174 ns 483827 ns 10 items_per_second=9.03011M/s
BM_PrintValueAsJSON_BalancedAttrs/16/3_stddev 5781 ns 5795 ns 10 items_per_second=107.483k/s
BM_PrintValueAsJSON_BalancedAttrs/16/3_cv 1.19 % 1.19 % 10 items_per_second=1.19%
BM_PrintValueAsJSON_PackageSet/1000_mean 271135 ns 270743 ns 10 items_per_second=3.69361M/s
BM_PrintValueAsJSON_PackageSet/1000_median 270766 ns 270355 ns 10 items_per_second=3.69884M/s
BM_PrintValueAsJSON_PackageSet/1000_stddev 1343 ns 1299 ns 10 items_per_second=17.6638k/s
BM_PrintValueAsJSON_PackageSet/1000_cv 0.50 % 0.48 % 10 items_per_second=0.48%
BM_PrintValueAsJSON_PackageSet/5000_mean 1589454 ns 1586677 ns 10 items_per_second=3.15127M/s
BM_PrintValueAsJSON_PackageSet/5000_median 1589694 ns 1586950 ns 10 items_per_second=3.1507M/s
BM_PrintValueAsJSON_PackageSet/5000_stddev 5485 ns 5455 ns 10 items_per_second=10.8287k/s
BM_PrintValueAsJSON_PackageSet/5000_cv 0.35 % 0.34 % 10 items_per_second=0.34%
BM_PrintValueAsJSON_PackageSet/10000_mean 3365166 ns 3359508 ns 10 items_per_second=2.97668M/s
BM_PrintValueAsJSON_PackageSet/10000_median 3369174 ns 3362968 ns 10 items_per_second=2.97356M/s
BM_PrintValueAsJSON_PackageSet/10000_stddev 15649 ns 15460 ns 10 items_per_second=13.7507k/s
BM_PrintValueAsJSON_PackageSet/10000_cv 0.47 % 0.46 % 10 items_per_second=0.46%
Bench code
commit 25643e9e4741cc0fb073fa398aaf8e9499833914
Author: Dyego Aurélio <dyegoaurelio@gmail.com>
Date: Sat May 23 13:31:07 2026 -0300
libexpr-tests: add value-to-json benchmark
Covers four value shapes:
DeepAttrs — linear chain of single-attr attrsets at depth 100/1k/5k
DeepLists — linear chain of single-element lists at depth 100/1k/5k
BalancedAttrs — balanced attrset tree at (breadth, depth) = (4,6),
(8,4), (16,3); each yields ~5k total nodes
PackageSet — wide top-level attrset of derivation-like entries with
outPath shortcut, at 1k/5k/10k entries; mimics the
shape of an evaluated hackage-packages.nix
diff --git a/src/libexpr-tests/meson.build b/src/libexpr-tests/meson.build
index 50d158209..55ed20527 100644
--- a/src/libexpr-tests/meson.build
+++ b/src/libexpr-tests/meson.build
@@ -100,6 +100,7 @@ if get_option('benchmarks')
'dynamic-attrs-bench.cc',
'get-drvs-bench.cc',
'regex-cache-bench.cc',
+ 'value-to-json-bench.cc',
)
benchmark_exe = executable(
diff --git a/src/libexpr-tests/value-to-json-bench.cc b/src/libexpr-tests/value-to-json-bench.cc
new file mode 100644
index 000000000..acb064ec8
--- /dev/null
+++ b/src/libexpr-tests/value-to-json-bench.cc
@@ -0,0 +1,196 @@
+#include <benchmark/benchmark.h>
+#include <nlohmann/json.hpp>
+
+#include "nix/expr/value-to-json.hh"
+#include "nix/expr/eval.hh"
+#include "nix/expr/eval-settings.hh"
+#include "nix/fetchers/fetch-settings.hh"
+#include "nix/store/store-open.hh"
+#include "nix/util/fmt.hh"
+
+namespace nix {
+
+static std::shared_ptr<EvalState> makeEvalState()
+{
+ static auto store = openStore("dummy://");
+ static fetchers::Settings fetchSettings;
+ static bool readOnlyMode = true;
+ static EvalSettings evalSettings = [] {
+ EvalSettings s{readOnlyMode};
+ s.nixPath = {};
+ return s;
+ }();
+ return std::make_shared<EvalState>(LookupPath{}, store, fetchSettings, evalSettings, nullptr);
+}
+
+// Build a linear chain of attrs: { a = { a = { ... { a = 0; } ... } } } of depth `depth`.
+static void buildDeepAttrs(EvalState & state, Value & root, size_t depth)
+{
+ Value * cur = &root;
+ auto sym = state.symbols.create("a");
+ for (size_t i = 0; i < depth; ++i) {
+ auto b = state.buildBindings(1);
+ auto & child = b.alloc(sym);
+ cur->mkAttrs(b.finish());
+ cur = &child;
+ }
+ cur->mkInt(0);
+}
+
+// Build a linear chain of lists: [[[ ... [ 0 ] ... ]]] of depth `depth`.
+static void buildDeepLists(EvalState & state, Value & root, size_t depth)
+{
+ Value * cur = &root;
+ for (size_t i = 0; i < depth; ++i) {
+ auto lb = state.buildList(1);
+ auto * child = state.allocValue();
+ lb[0] = child;
+ cur->mkList(lb);
+ cur = child;
+ }
+ cur->mkInt(0);
+}
+
+// Build a balanced attrset tree: each node has `names.size()` attrs, recursing `depth` levels.
+// Total node count = breadth^depth.
+static void buildBalancedAttrs(EvalState & state, Value & v, const std::vector<std::string> & names, size_t depth)
+{
+ if (depth == 0) {
+ v.mkInt(0);
+ return;
+ }
+ auto b = state.buildBindings(names.size());
+ for (auto & n : names) {
+ auto & child = b.alloc(state.symbols.create(n));
+ buildBalancedAttrs(state, child, names, depth - 1);
+ }
+ v.mkAttrs(b.finish());
+}
+
+static void BM_PrintValueAsJSON_DeepAttrs(benchmark::State & bstate)
+{
+ auto statePtr = makeEvalState();
+ auto & state = *statePtr;
+ const auto depth = static_cast<size_t>(bstate.range(0));
+ Value root;
+ buildDeepAttrs(state, root, depth);
+
+ NixStringContext context;
+ for (auto _ : bstate) {
+ auto j = printValueAsJSON(state, /*strict=*/true, root, noPos, context, /*copyToStore=*/false);
+ benchmark::DoNotOptimize(j);
+ }
+ bstate.SetItemsProcessed(bstate.iterations() * depth);
+}
+
+BENCHMARK(BM_PrintValueAsJSON_DeepAttrs)->Arg(100)->Arg(1'000)->Arg(5'000);
+
+static void BM_PrintValueAsJSON_DeepLists(benchmark::State & bstate)
+{
+ auto statePtr = makeEvalState();
+ auto & state = *statePtr;
+ const auto depth = static_cast<size_t>(bstate.range(0));
+ Value root;
+ buildDeepLists(state, root, depth);
+
+ NixStringContext context;
+ for (auto _ : bstate) {
+ auto j = printValueAsJSON(state, /*strict=*/true, root, noPos, context, /*copyToStore=*/false);
+ benchmark::DoNotOptimize(j);
+ }
+ bstate.SetItemsProcessed(bstate.iterations() * depth);
+}
+
+BENCHMARK(BM_PrintValueAsJSON_DeepLists)->Arg(100)->Arg(1'000)->Arg(5'000);
+
+static void BM_PrintValueAsJSON_BalancedAttrs(benchmark::State & bstate)
+{
+ auto statePtr = makeEvalState();
+ auto & state = *statePtr;
+ const auto breadth = static_cast<size_t>(bstate.range(0));
+ const auto depth = static_cast<size_t>(bstate.range(1));
+
+ std::vector<std::string> names;
+ names.reserve(breadth);
+ for (size_t i = 0; i < breadth; ++i)
+ names.push_back(fmt("k%|1$04d|", i));
+
+ Value root;
+ buildBalancedAttrs(state, root, names, depth);
+
+ size_t totalNodes = 1;
+ size_t p = 1;
+ for (size_t i = 0; i < depth; ++i) {
+ p *= breadth;
+ totalNodes += p;
+ }
+
+ NixStringContext context;
+ for (auto _ : bstate) {
+ auto j = printValueAsJSON(state, /*strict=*/true, root, noPos, context, /*copyToStore=*/false);
+ benchmark::DoNotOptimize(j);
+ }
+ bstate.SetItemsProcessed(bstate.iterations() * totalNodes);
+}
+
+BENCHMARK(BM_PrintValueAsJSON_BalancedAttrs)
+ ->ArgPair(4, 6) // ~5k nodes
+ ->ArgPair(8, 4) // ~4.7k nodes
+ ->ArgPair(16, 3); // ~4.4k nodes
+
+// Build a "package set" shape: a wide top-level attrset where every entry is
+// a small derivation-like attrset (pname, version, description, license,
+// broken, outPath). The outPath attr triggers the JSON serializer's shortcut
+// to emit only the store path. Mimics the shape of nixpkgs/.../hackage-packages.nix
+// after evaluation.
+static void buildPackageSet(EvalState & state, Value & root, size_t numPackages)
+{
+ auto pnameSym = state.symbols.create("pname");
+ auto versionSym = state.symbols.create("version");
+ auto descSym = state.symbols.create("description");
+ auto licenseSym = state.symbols.create("license");
+ auto brokenSym = state.symbols.create("broken");
+ auto outPathSym = state.s.outPath;
+
+ auto topB = state.buildBindings(numPackages);
+ for (size_t i = 0; i < numPackages; ++i) {
+ auto pkgB = state.buildBindings(6);
+
+ auto & op = pkgB.alloc(outPathSym);
+ op.mkString(fmt("/nix/store/000000000000000000000000000000000-pkg-%|1$06d|-1.2.3", i), state.mem);
+ auto & pn = pkgB.alloc(pnameSym);
+ pn.mkString(fmt("pkg-%|1$06d|", i), state.mem);
+ auto & ver = pkgB.alloc(versionSym);
+ ver.mkString("1.2.3", state.mem);
+ auto & d = pkgB.alloc(descSym);
+ d.mkString("A benchmark package", state.mem);
+ auto & l = pkgB.alloc(licenseSym);
+ l.mkString("BSD3", state.mem);
+ auto & b = pkgB.alloc(brokenSym);
+ b.mkBool(false);
+
+ auto & pkg = topB.alloc(state.symbols.create(fmt("pkg-%|1$06d|", i)));
+ pkg.mkAttrs(pkgB.finish());
+ }
+ root.mkAttrs(topB.finish());
+}
+
+static void BM_PrintValueAsJSON_PackageSet(benchmark::State & bstate)
+{
+ auto statePtr = makeEvalState();
+ auto & state = *statePtr;
+ const auto numPackages = static_cast<size_t>(bstate.range(0));
+ Value root;
+ buildPackageSet(state, root, numPackages);
+
+ NixStringContext context;
+ for (auto _ : bstate) {
+ auto j = printValueAsJSON(state, /*strict=*/true, root, noPos, context, /*copyToStore=*/false);
+ benchmark::DoNotOptimize(j);
+ }
+ bstate.SetItemsProcessed(bstate.iterations() * numPackages);
+}
+
+BENCHMARK(BM_PrintValueAsJSON_PackageSet)->Arg(1'000)->Arg(5'000)->Arg(10'000);
+
+} // namespace nix
f4b2fbc to
e74168f
Compare
Previously, `builtins.toJSON` on a self-referential value (e.g.
`let a = { b = a; }; in a`) crashed the evaluator with a stack
overflow because `printValueAsJSON` recursed unboundedly.
Track visited Bindings (for attrsets) and Value pointers (for lists)
in a scoped seen-set; on a collision, throw `InfiniteRecursionError`.
To name both ends of the cycle in the error (e.g. `the value at
.branch.twig is the same as the value at .branch`), the walk runs
twice: recording a path for every node is overhead that's wasted
on cycle-free values, so the first pass uses a minimal seen-set and
throws an internal sentinel on collision, and only the re-run
records paths to build the rich message.
Mirrors the cycle detection added for `printValueAsJSON`. The XML walker already handled cycles for derivations (via `drvsSeen` keyed on `drvPath`) but not for arbitrary attrsets or lists, which could loop indefinitely on self-referential values. Throws `InfiniteRecursionError` on collision, naming both ends of the cycle (e.g. `the value at .branch.twig is the same as the value at .branch`) via the same two-pass walk used by `printValueAsJSON`: the fast pass streams XML to the user's `ostream` and throws an internal sentinel on collision; the wrapper then re-runs against a discarded `XMLWriter` with path tracking to build the rich message.
e74168f to
058b490
Compare
Motivation
Serializing a self-referential value such as
let a = { b = a; }; in awasbroken in two ways depending on the target:
builtins.toJSONandnix eval --jsonfailed with an opaquestack overflow; max-call-depth exceedederror.nix-instantiate --xmlstreamed thousands of nested<attrs>/<list>elements to stdout
Context
Fixes #12289.
Add 👍 to pull requests you find important.
The Nix maintainer team uses a GitHub project board to schedule and track reviews.