-
-
Notifications
You must be signed in to change notification settings - Fork 14.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NixOS always imports all modules #137168
Comments
I'm sure I already saw an issue or at least some discussion regarding this recently... Can't remember where exactly though. |
It comes up in discussions every now and then, but usually it dies down because of scope creep and/or the prospect of a large and/or invasive change. I'd be interested to know of past issues, threads, etc, so I can address any concerns from those. I couldn't find an issue for this idea, but maybe I searched wrong. |
Ah, it was this one: #57477 |
The problem with modules is just loading them can have side effects (even though well-designed modules put all functionality behind enable flags.) This means they can't be lazily evaluated like packages, which would be the ideal imo. |
This is gathering more attention than I expected. @jbalme That's a correct intuition for why we can't fix this with a localized change in the module system in @rnhmjoj Thanks! It seems that these efforts were cut short because of flakes. Flakes are a fairly low level system of distributing expressions, not modules specifically. Flakes solve a problem that is largely independent: moving the files, whereas the performance can be fixed by refactoring the files. Such refactoring needs to happen before the modules can be moved, so it is not a wasted effort. That said, I do think we tend to underestimate the value of having a somewhat coherent distribution of modules. We'll lose that if we switch entirely. Making broad changes to the NixOS modules will be much more painful because all the individual maintainers need to reinvent the such migrations. Flakes could be extended with versioning features perhaps to relieve some of the pain. Per-repo tooling is also a big cost. What my issue is about Basically, RFC 22 can be picked up. I've considered working on this issue in coming December, but the restructuring technique employed should be agreed on for such an effort to be effective. I'll describe the steps summarily here: The first goal is to provide a new entrypoints into NixOS that do not make use of the complete module list. I'd probably start with a module that builds To bring the With this core in place, new features can be refactored to make their closures minimal too. Eventually, An illustration of what the
Notable changes when applying this pattern
There's a couple of solutions and non-issues that I'm forgetting to write about right now. What's next? I'm glad I've shared my ideas, but I won't be able to spend much time at all on it in the coming months. On the plus side, this means that if you want to give it a go, it won't be a duplicated effort. Not that it would matter, because it's probably a good learning experience. |
It is possible to do dynamic The significant limitation is that it is staged. The modules that are dynamically imported can not perform dynamic imports themselves. All the logic that could produce Proof of Concept: modules producing dynamic imports with a necessarily limited scope of writing
let
coreModule =
{ lib, config, ... }:
{
options = {
importList = lib.mkOption {
type = lib.types.listOf lib.types.unspecified;
};
configuration = lib.mkOption {
type = lib.types.submoduleWith { modules = [ initSystemModule ]; };
};
};
config = {
configuration = { ... }: {
imports = config.importList;
};
};
};
serviceModule =
{ lib, config, ... }: {
options = {
foo.enable = lib.mkEnableOption "foo";
};
config = lib.mkIf config.foo.enable {
importList = [ fooModule ];
};
};
initSystemModule = { lib, ... }: {
options = {
services = lib.mkOption {
type = lib.types.listOf lib.types.str;
};
};
};
fooModule = { ... }: {
config = {
services = [ "foo" ];
};
};
# something like configuration.nix
configurationModule = { ... }: {
foo.enable = true;
};
lib = import ./lib;
in lib.evalModules { modules = [ coreModule serviceModule configurationModule ]; }
$ nix repl staged-imports.nix
nix-repl> config.configuration.services
[ "foo" ]
|
hi @roberth 👋 until this issue is resolved what is my best course of action? specify a baseModules argument to eval-config.nix which is a whittled down list of exactly what i need? |
@aanderse While that is a possibility, it is a bit fragile, because it will not take into account new mandatory modules. Usually that leads to an evaluation error, but if not, it could be very bad for your deployment. To do it properly, I think we should at least identify two sets of modules and separate those in Nixpkgs:
That way, we put an upper bound on how bad it could get. To make it work well in general, this concept needs more buy-in from the community. I've done some experimental work which is only used in some tests as of now.
While this works well enough, it is not well known. That's not a problem in itself, but something to be aware of. The crucial thing that's missing right now, is an actually useful and meaningful By solving these problems, we have a coherent story for supporting both traditional and minimal use of modules, and we can explain why the small changes we need are useful. For instance, adding traditionally redundant imports currently leads to some resistance and some might even "clean them up". |
thanks for the helpful response @roberth - as always, very thorough ❤️ |
This logic is not only related to performance. It can be problematic (and even dangerous sometimes) See an example just found: #305304 |
Would it be possible to construct the import graph using the module system's existing options? Whenever a service needs i.e. postgres, they set Of course such optional modules would have some limitations such that That wouldn't be a silver bullet fixing performance issues all at once but it should at least remove an entire category of modules (optional services you don't care about) from the user's module system eval. A further step that could be done would be to broaden the enablement condition to also include arbitrary options under the module's "namespace" being set. The foo module could be imported when |
I have to note, conditional importing sounds like an absolute nightmare for debuggability |
What specific issues do you forsee? It should also be possible to have an option to turn this optimisation off and always import the full list of modules, regardless of enablement. I also found out that I wasn't the first person to think of this general idea: #57477 (comment) |
I totally thought so. Let's talk about it in a few days. |
@Atemu it requires that you either have Fixpoint iteration on top of the fixpoint we havePerform fixpoint iteration on top of the lazy fixpoint we already have for the config. This may indeed be horrible for debugging, because you would have to consider whether the error occurs during which phase or iteration. Also fixpoint iteration relies on a termination condition that produces a boolean, so you'd have to make huge Better use of submodulesDo the expensive stuff in a submodule. It is perfectly fine for a module to control what goes into its submodule(s), with all sorts of conditions and whatnot, as long as those don't depend on the evaluation of that submodule. This way we don't need new complicated generic machinery; the module system tools we have suffice. This is also what I've proposed here. The "portability" aspect of that may well be a minor detail. I've jokingly referred to it as the Importable Service Layer, and if you un-split some modules and rename some things, that's actually what it is. It does cause changes to the option structure, but that is actually beneficial because it adds the ability to have multiple instances of a service to all services in a consistent manner. It's also possible to remain interface compatible by creating a few options that forward their definitions to a service module with a hardcoded "default" name, such as
|
I think it is a good idea to define a base set of modules. For some reason the set of necessary modules for evaluation != the set of modules needed for a proper system. Or rather, the minimal set of modules for evaluation. |
For eval this is currently all you need, more or less: What you mean by the "set of modules needed for a proper system" is unclear here. |
If you try to minimize the number of modules imported as the metric for reducing modules, you will end up with an inoperable system, i.e. in the event where there is no option evaluated and the config is always active Your profile does not reduce the number of modules imported. |
Describe the bug
NixOS always imports all modules, leading to degrading performance as NixOS grows.
Steps To Reproduce
Steps to reproduce the behavior:
But to really see the potential:
modules-list.nix
and fix a few broken options referencestoplevel
go down from 4.0 to 1.4 seconds.See proof of concept: hercules-ci@97dcbe8
Expected behavior
New modules don't reduce NixOS evaluation performance. NixOS can scale in complexity.
Additional context
I'll probably write an RFC suggesting the required refactorings and module authoring guidelines. This can all be achieved in a backcompatible manner, but the performance gains may be require some opting in.
Notify maintainers
@infinisil
Metadata
Please run
nix-shell -p nix-info --run "nix-info -m"
and paste the result.Maintainer information:
The text was updated successfully, but these errors were encountered: