Fix quadratic key disambiguation in lock files - #16387
Merged
Merged
Conversation
xokdvium
reviewed
Aug 29, 2026
LockFile::toJSON() assigns each node a unique key, disambiguating
collisions by appending _2, _3, and so on. The search restarted at 2 on
every collision, so the k-th node sharing a base name cost k iterations
and the whole pass was O(n^2) in the number of colliding nodes.
This is easy to hit in practice. Every flake that depends on many other
flakes accumulates one "systems", "flake-compat" or "nixpkgs" node per
dependency, and they all share a base name. Because the serialized lock
file is passed to call-flake.nix on every evaluation, the cost is paid
by `nix eval`, `nix flake metadata` and `nix flake lock` alike, not just
when writing the file back out.
Track the highest suffix already handed out per base key and resume from
there. The insert check is retained, so a name that legitimately appears
in the lock file (e.g. an input actually called "systems_2") is still
skipped over and the resulting keys are unchanged.
Measured on a flake with n inputs that each pull in their own "systems",
timing `nix eval` of an output that touches no input:
inputs nodes before after speedup
500 1000 0.64s 0.15s 4.3x
1000 2000 0.88s 0.16s 5.5x
2000 4000 3.05s 0.28s 10.9x
4000 8000 11.49s 0.54s 21.3x
Lock files produced before and after are byte-identical, verified on the
cases above, on a lock file containing pre-existing "systems_2",
"systems_3" and "systems_5" inputs, and on an existing real-world flake.
Co-authored-by: Sergei Zimmerman <sergei@zimmerman.foo>
fzakaria
force-pushed
the
flake-lock-quadratic-key-dedup
branch
from
August 29, 2026 15:11
0db9cf4 to
c0dfd54
Compare
xokdvium
approved these changes
Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
I am working on a new idea 💡 and I ran into this performance problem when evaluating many Nix flakes.
LockFile::toJSON()gives every node a unique key, disambiguating collisions by appending_2,_3, … The search restarts at2on every collision:So the k-th node sharing a base name costs k iterations, making the pass O(n²) in the number of colliding nodes.
This is easy to hit in practice. A flake that depends on many other flakes accumulates one
systems/flake-compat/nixpkgsnode per dependency, and they all share a base name. In a 4000-input flake, 3999 nodes are namedsystems_2…systems_4000, costing ~8Mfmt()calls.The cost is not confined to writing the file. flake.cc passes the serialized lock to
call-flake.nixon every evaluation:nix eval,nix flake metadataandnix flake lockare all O(n²) for this name determinationContext
I hit this building a flake with a very large number of inputs. Evaluating an output that touches no input at all took 11.5s, which was surprising given inputs are lazy.
The fix
Track the highest suffix already handed out per base key and resume from there instead of rescanning from 2. The
keys.insertcheck is retained, so a name that legitimately appears in the lock file (an input actually calledsystems_2) is still skipped and the resulting keys are unchanged.Benchmarks
A flake with n inputs that each pull in their own
systems, timingnix evalof an output touching no input:The before column is quadratic (2x inputs → ~3.8x time); the after column is linear.
Reproducer:
Correctness
Lock files produced before and after are byte-identical, verified on:
systems_2,systems_3andsystems_5inputs interleaved with 41 auto-disambiguatedsystemsnodesflake.lock, regenerated and comparedPriorities
Add 👍 to pull requests you find important.