Changelog: Lazy trees (faster Nix builds)

2 hours ago 2

Lazy trees have been one of the most hotly requested Nix features for quite some time. They make Nix much more efficient in larger repositories, particularly in massive monorepos. And so we’re excited to announce that lazy trees have landed in Determinate Nix version 3.5.2, based on version 2.28.3 of upstream Nix.

We’re confident that this initial release of lazy trees should produce few issues for users. But for the sake of caution, lazy trees is available solely on an opt-in basis for the time being. To start using it today, first install or upgrade Determinate Nix and enable lazy trees in your custom Nix configuration:

Our co-founder Eelco Dolstra currently has a pull request open to get lazy trees into upstream Nix and we hope to see that merged soon.

Why lazy trees are important

In a nutshell, lazy trees provide faster and less resource-intensive evaluation in many standard usage scenarios. For evaluations inside of the Nixpkgs repo, for example, we’ve frequently seen reductions in wall time of 3x or more and reductions in disk usage of 20x or more—and occasionally reductions far beyond even this.

With lazy trees enabled, Nix scopes file copying to what the specific expression demands—and nothing more. More specifically, Nix uses a virtual filesystem to gather the necessary file state prior to copying anything to the Nix store, “mounting” lazy source inputs to this virtual filesystem at /nix/store/<random-hash>.

This provides a much more parsimonious mode of operation that should quietly make just about everything in Nix better, from standard package builds to development environments to NixOS deployment and beyond.

If you’d like to see this in action, try evaluating something in Nixpkgs yourself. Install or upgrade Determinate Nix and run this sequence of commands:

# Clone Nixpkgs

git clone https://github.com/NixOS/nixpkgs

cd nixpkgs

# Evaluate the cowsay derivation with lazy trees disabled

time nix eval \

--no-eval-cache \

.#cowsay

# Make a meaningless change to the source tree

echo "" >> flake.nix

# Evaluate the cowsay derivation again

time nix eval \

--no-eval-cache \

.#cowsay

Adding an empty line to flake.nix (a meaningless change) invalidates the evaluation cache and forces Nix to copy the entire source tree to the Nix store, otherwise the second evaluation would be faster.

Now the same sequence of steps with lazy trees:

# Enable lazy trees

echo "lazy-trees = true" | sudo tee -a /etc/nix/nix.custom.conf

# Make a meaningless change to the source tree

echo "" >> flake.nix

# Evaluate the cowsay derivation

time nix eval \

--no-eval-cache \

.#cowsay

# Make a meaningless change to the source tree

echo "" >> flake.nix

# Evaluate the cowsay derivation again

time nix eval \

--no-eval-cache \

.#cowsay

You’ll notice two things:

  1. The evaluation should be substantially faster with lazy trees (if not, please let us know via email or on Discord).
  2. With lazy trees, you won’t see any indication that Nix is copying the source tree to the Nix store. That’s because it’s able to determine, via evaluation, that only the sources for the cowsay package are necessary to achieve its ends.

Saving time is good. Also good: using less disk space. I’ll provide an example.

First, let’s evaluate something in Nixpkgs using a ./tmp subdirectory inside the current directory as a fresh Nix store:

# Disable lazy trees

echo "lazy-trees = false" | sudo tee -a /etc/nix/nix.custom.conf

# Evaluate the cowsay package

nix eval \

--no-eval-cache \

--store ./tmp \

.#cowsay

# See how much disk space is used by the local Nix store

du -sh ./tmp

On my machine, this yields 304 megabytes. Now let’s try the same thing with lazy trees enabled:

# Delete the local Nix store to start from scratch

sudo rm -rf ./tmp

# Enable lazy trees

echo "lazy-trees = true" | sudo tee -a /etc/nix/nix.custom.conf

# Evaluate the cowsay package

nix eval \

--no-eval-cache \

--store ./tmp \

.#cowsay

# See how much disk space is used by the local Nix store

du -sh ./tmp

# Delete the local Nix store to clean up

sudo rm -rf ./tmp

This yields 13 megabytes. That’s over 23 times less disk space. This specific number isn’t generalizable across all situations, of course, but it does illustrate just how much more efficient Nix can become in a standard scenario when using lazy trees.

CI comparisons

To illustrate some of these differences in a CI environment, I’ve set up a GitHub repo at DeterminateSystems/lazy-trees-comparisons. The workflow configuration shows the sequences of steps involved (similar to the steps above).

As an example, check out this CI run, which evaluates the stdenv attribute of Nixpkgs on GitHub Actions’ ubuntu-latest runner. The evaluation time without lazy trees:

real 0m10.787s

user 0m3.655s

sys 0m6.759s

Wall time without lazy trees: almost 11 seconds. Disk usage without lazy trees: 433 megabytes.

Evaluation time with lazy trees:

real 0m3.500s

user 0m1.762s

sys 0m1.701s

Wall time without lazy trees: 3.5 seconds. Disk usage with lazy trees: 11 megabytes.

Again, a substantial difference. This is, of course, just one result of one run, but it’s consistent with more general runs and we encourage you to make your own comparisons.

New warning

When using Determinate Nix with lazy trees, you may see warnings like this:

warning: Performing inefficient double copy of path '«github:nixos/nixpkgs/0c0bf9c057382d5f6f63d54fd61f1abd5e1c2f63»/' to the store. This can typically be avoided by rewriting an attribute like `src = ./.` to `src = builtins.path { path = ./.; name = "source"; }`.

You’ll see this in cases where you specify the root of a repo as the source for a derivation. When you use ./. as the source for a derivation, Nix copies your repo to the Nix store under the name /nix/store/<random-hash>-source, which is then copied again as /nix/store/<random-hash>-<other-random-hash>-source.

Because of that, this is an inefficient way to specify your source:

{

myPackage = myDerivationFunction {

src = ./.;

};

}

This is a much more efficient way:

{

myPackage = myDerivationFunction {

src = builtins.path { path = ./.; name = "my-package-source"; };

};

}

The name field can be whatever you like. In a flake, you can also use self instead of ./., as in:

{

myPackage = myDerivationFunction {

src = self;

};

}

Ideally, this wouldn’t be necessary and using ./. wouldn’t produce any inefficiency. But unfortunately, changing this behavior would constitute a backwards-incompatible change that we are highly averse to introducing.

How to use lazy trees in GitHub Actions

If you’d like to try out lazy trees in GitHub Actions, you can use our Determinate Nix Action and supply lazy-trees = true in your configuration:

steps:

- uses: actions/checkout@v4

- name: Install Determinate Nix with lazy trees enabled

uses: DeterminateSystems/determinate-nix-action@v3

with:

extra-conf:

lazy-trees = true

- name: Set up FlakeHub Cache

uses: DeterminateSystems/flakehub-cache-action@main

- name: Build default package output

run: nix build .

How to upgrade or install Determinate Nix

If you already have Determinate Nix installed, you can upgrade to 3.5.2 with one Determinate Nixd command:

sudo determinate-nixd upgrade

If you don’t yet have Determinate Nix installed, you can install it on macOS using our graphical installer:

Logo for graphical installer

Install Determinate Nix on macOS now

Apple Silicon and Intel

On Linux:

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | \

sh -s -- install --determinate

On NixOS, we recommend using our dedicated NixOS module.

To verify that you’re on the most recent version of Determinate Nix at any time:

And again, once you are on the most recent version, you can enable lazy trees in your custom Nix configuration:

More to come

Determinate Nix has introduced a number of improvements to Nix in recent months, including JSON logging and automatic fixes for hash mismatches, but we’re confident that the lazy trees release presents the most consequential change to date because it establishes a new, qualitatively improved baseline for Nix performance.

But we are far from finished with improving Nix’s performance. In the coming months, we intend to ship more improvements to Nix’s evaluation performance, including:

  1. Improving evaluation caching
  2. Bringing parallel evaluation to more Nix operations
  3. Providing multi-threaded unpacking of flakes, both flakes from tarballs (like those from FlakeHub) and those from source forges like GitHub.

Stay tuned!

Read Entire Article