MRE for "1000 instances of nixpkgs" problem

Hello,

@PhilipTaron recently informed about the potential drawbacks of using import nixpkgs { inherit system; } as opposed to nixpkgs.legacyPackages.${system}, with some discussion in Refactorings and updates by philiptaron · Pull Request #26 · timokau/nix-bisect · GitHub

After reading these, my basic understanding is summarized here, summarized as:

However, were there multiple instances of import nixpkgs, it looks like nix doesn’t cache the result of this function call, so even identical import nixpkgs {...} calls will be separately evaluated (costing memory and time at evaluation time)

I decided to write a MRE to wrap my head around the issue; @PhilipTaron graciously suggested an approach, and you can my hacky implementation here (sorry, I know there are a few crimes there).

Oddly, I’m finding that the legacyPackages pattern is using nearly twice as much memory as import nixpkgs, when I thought that this was supposed to demonstrate the exact opposite. I initially was not

$ nix run --no-write-lock-file \
    git+https://gist.github.com/n8henrie/85a391f85d9628a09c7f99c3db41a62d
+ /nix/store/1fj5swf9636zxksrab3cg44mg4azlsj3-test-import/bin/test-import
        Maximum resident set size (kbytes): 86800
+ /nix/store/0arw20ij4fwpp5lz9hna2azq16dqw8d6-test-legacyPackages/bin/test-legacyPackages
        Maximum resident set size (kbytes): 137028

Tested on aarch64-darwin and x86_64-linux.

Am I misunderstanding the principle here? Or has something changed, and import nixpkgs is no longer a memory hog for consumers relative to legacyPackages?

Thanks for any input!

3 Likes

Unless I’m missing something, this isn’t using the follows convention for the nixpkgs inputs.

I’m curious if the results would be different if you added something like the below:
echo "inputs.import-$num.inputs.nixpkgs.follows = \"nixpkgs\";" >> flake.nix

I had considered that, but it was discussed in one of the threads linked above and thought not to ameliorate the problem.

Further, I would expect the absence of follows to (if anything) worsen the result of the import style, which was supposed to be substantially worse off for this MRE in the first place, but instead is already showing lower memory usage. The question is “why is the import style using less memory instead of (much) more?”

I tested it and you’re right, the difference is noise

Original gist:

> nix run
+ /nix/store/a9xqxjbc6hjf3c6r1dnsps4rn5qd38v4-test-import/bin/test-import
        Maximum resident set size (kbytes): 95000
+ /nix/store/0arw20ij4fwpp5lz9hna2azq16dqw8d6-test-legacyPackages/bin/test-legacyPackages
        Maximum resident set size (kbytes): 145944

After adding echo "inputs.import-$num.inputs.nixpkgs.follows = \"nixpkgs\";" >> flake.nix:

> nix run
+ /nix/store/a9xqxjbc6hjf3c6r1dnsps4rn5qd38v4-test-import/bin/test-import
        Maximum resident set size (kbytes): 95556
+ /nix/store/0arw20ij4fwpp5lz9hna2azq16dqw8d6-test-legacyPackages/bin/test-legacyPackages
        Maximum resident set size (kbytes): 145776

If I change line 92 in your gist from
echo '# fake reference to ''${self.inputs.import-'"$num}" >> flake.nix
to
echo '# fake reference to ''${self.inputs.import-'"$num.packages.x86_64-linux.default}" >> flake.nix
things look a little different.

> nix run
+ /nix/store/v81ir878cdrfislmq690n2hahhd07snn-test-import/bin/test-import
        Maximum resident set size (kbytes): 4090012
+ /nix/store/0arw20ij4fwpp5lz9hna2azq16dqw8d6-test-legacyPackages/bin/test-legacyPackages
        Maximum resident set size (kbytes): 146448

EDIT: I actually needed to edit line 133 also, but the results are correct

Nice. Can you write a write-up once you stabilize your findings? I think a lot of people are referring to my article. Unfortunately, I don’t have much time to do the write-up myself, but I’d like to at least provide a link to the new information.

2 Likes

tl;dr: application are only good for command line; overlays are good for composability; flakes application should be inherited from the usage of their overlays.

While having different instances of nixpkgs make sense for command line convenience, the problem of the 1000 nixpkgs comes from the usage of application directly from input flakes.

As opposed to nixpkgs instances, overlays are stackable and mostly composable. If you are building a flake which aggregates multiple others, then you should rely on the overlays instead of applications provided by other flakes.

Flakes as they are used today, come with application as the first class citizen, but they are merely conveniences that are good to expose to end users. Instead a flake should provide an overlay which adds all its application and dev-shells, and then just inherits its applications/dev-shell from one instance of nixpkgs.

The lack of introspection of overlays, mentioned against overlay in the blog post, is a non-issue if all applications provided via the overlays are also be exposed by the flake as application.

One example from inventree packaging that I contributed to recently show case how to design such a flake. (despite nix-community/pip2nix not providing an overlay :person_facepalming:)

If all flakes were designed around overlays instead of applications, we should be able to replace the nixpkgs.follows by a minimal version of Nixpkgs which only contains lib, and we would not have this false problem of 1000 Nixpkgs instances.

3 Likes

@samhug is right – thank you! My fake reference of the flake was enough, I had to specify a package to get through enough laziness.

I’ve updated the gist to include the follows and to fix the above issue: flake.nix · GitHub

$ nix run --no-write-lock-file \
    git+https://gist.github.com/n8henrie/85a391f85d9628a09c7f99c3db41a62d
warning: not writing modified lock file of flake 'git+https://gist.github.com/n8henrie/85a391f85d9628a09c7f99c3db41a62d':
• Added input 'nixpkgs':
    'github:nixos/nixpkgs/bb0ac7022fb74b2d4e26153109bc1d5428fca356' (2024-04-28)
+ /nix/store/f5fkbvz3iqnycnq8h00d43mbzxxf8qw3-test-import/bin/test-import
        Maximum resident set size (kbytes): 4423072
+ /nix/store/y4q904nkf0y3jrkm7pl6zzbgqlk353vk-test-legacyPackages/bin/test-legacyPackages
        Maximum resident set size (kbytes): 444624
$ awk 'BEGIN { print 4423072 / 444624 }'
9.94789

@PhilipTaron @samhug @zimbatm – does this look like what you expected? 100x of import nixpkgs (as opposed to nixpkgs.legacyPackages) leading to a 10x increase in max memory usage?